diff --git a/frontend/components/Editor/Editor.js b/frontend/components/Editor/Editor.js index 6a99e730f..75ed07f1b 100644 --- a/frontend/components/Editor/Editor.js +++ b/frontend/components/Editor/Editor.js @@ -1,15 +1,17 @@ // @flow import React, { Component } from 'react'; +import { observable } from 'mobx'; import { observer } from 'mobx-react'; import { Editor, Plain } from 'slate'; 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 Flex from 'components/Flex'; import ClickablePadding from './components/ClickablePadding'; import Toolbar from './components/Toolbar'; import BlockInsert from './components/BlockInsert'; import Placeholder from './components/Placeholder'; +import Contents from './components/Contents'; import Markdown from './serializer'; import createSchema from './schema'; import createPlugins from './plugins'; @@ -37,10 +39,7 @@ type KeyData = { editor: EditorType; schema: Object; plugins: Array; - - state: { - state: State, - }; + @observable editorState: State; constructor(props: Props) { super(props); @@ -51,10 +50,10 @@ type KeyData = { onImageUploadStop: props.onImageUploadStop, }); - if (props.text) { - this.state = { state: Markdown.deserialize(props.text) }; + if (props.text.trim().length) { + this.editorState = Markdown.deserialize(props.text); } else { - this.state = { state: Plain.deserialize('') }; + this.editorState = Plain.deserialize(''); } } @@ -74,15 +73,16 @@ type KeyData = { } } - onChange = (state: State) => { - this.setState({ state }); - }; + onChange = (editorState: State) => { + if (this.editorState !== editorState) { + this.props.onChange(Markdown.serialize(editorState)); + } - onDocumentChange = (document: Document, state: State) => { - this.props.onChange(Markdown.serialize(state)); + this.editorState = editorState; }; handleDrop = async (ev: SyntheticEvent) => { + if (this.props.readOnly) return; // check if this event was already handled by the Editor if (ev.isDefaultPrevented()) return; @@ -92,7 +92,9 @@ type KeyData = { const files = getDataTransferFiles(ev); 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(); transform.collapseToStartOf(state.document); transform.focus(); - this.setState({ state: transform.apply() }); + this.editorState = transform.apply(); }; focusAtEnd = () => { @@ -170,7 +172,7 @@ type KeyData = { const transform = state.transform(); transform.collapseToEndOf(state.document); transform.focus(); - this.setState({ state: transform.apply() }); + this.editorState = transform.apply(); }; render = () => { @@ -187,11 +189,12 @@ type KeyData = { >
+ {!readOnly && - } + } {!readOnly && } @@ -202,10 +205,9 @@ type KeyData = { schema={this.schema} plugins={this.plugins} emoji={emoji} - state={this.state.state} + state={this.editorState} onKeyDown={this.onKeyDown} onChange={this.onChange} - onDocumentChange={this.onDocumentChange} onSave={onSave} readOnly={readOnly} /> @@ -246,22 +248,6 @@ const StyledEditor = styled(Editor)` h5, h6 { font-weight: 500; - - .anchor { - visibility: hidden; - color: #dedede; - padding-left: 0.25em; - } - - &:hover { - .anchor { - visibility: visible; - - &:hover { - color: #cdcdcd; - } - } - } } h1:first-of-type { diff --git a/frontend/components/Editor/components/Code.js b/frontend/components/Editor/components/Code.js index d6e2d5385..f370392d0 100644 --- a/frontend/components/Editor/components/Code.js +++ b/frontend/components/Editor/components/Code.js @@ -6,11 +6,13 @@ import { color } from 'styles/constants'; import type { Props } from '../types'; export default function Code({ children, node, readOnly, attributes }: Props) { + const language = node.data.get('language') || 'javascript'; + return ( {readOnly && } -
-        
+      
+        
           {children}
         
       
@@ -20,7 +22,7 @@ export default function Code({ children, node, readOnly, attributes }: Props) { const Pre = styled.pre` padding: .5em 1em; - background: ${color.smoke}; + background: ${color.smokeLight}; border-radius: 4px; border: 1px solid ${color.smokeDark}; diff --git a/frontend/components/Editor/components/Contents.js b/frontend/components/Editor/components/Contents.js new file mode 100644 index 000000000..7a1f19a44 --- /dev/null +++ b/frontend/components/Editor/components/Contents.js @@ -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 { + 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 ( + + + {this.headings.map(heading => { + const slug = headingToSlug(heading); + const active = this.activeHeading === slug; + + return ( + + + {heading.text} + + + ); + })} + + + ); + } +} + +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; diff --git a/frontend/components/Editor/components/Heading.js b/frontend/components/Editor/components/Heading.js index c8ceb4464..29a7ccc37 100644 --- a/frontend/components/Editor/components/Heading.js +++ b/frontend/components/Editor/components/Heading.js @@ -2,13 +2,12 @@ import React from 'react'; import { Document } from 'slate'; import styled from 'styled-components'; -import _ from 'lodash'; -import slug from 'slug'; +import headingToSlug from '../headingToSlug'; import type { Node, Editor } from '../types'; import Placeholder from './Placeholder'; type Props = { - children: React$Element, + children: React$Element<*>, placeholder?: boolean, parent: Node, node: Node, @@ -31,7 +30,7 @@ function Heading(props: Props) { const parentIsDocument = parent instanceof Document; const firstHeading = parentIsDocument && parent.nodes.first() === node; const showPlaceholder = placeholder && firstHeading && !node.text; - const slugish = _.escape(`${component}-${slug(node.text)}`); + const slugish = headingToSlug(node); const showHash = readOnly && !!slugish; const Component = component; const emoji = editor.props.emoji || ''; @@ -40,8 +39,10 @@ function Heading(props: Props) { emoji && title.match(new RegExp(`^${emoji}\\s`)); return ( - - {children} + + + {children} + {showPlaceholder && {editor.props.placeholder} @@ -53,7 +54,7 @@ function Heading(props: Props) { const Wrapper = styled.div` display: inline; - margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)} + margin-left: ${(props: Props) => (props.hasEmoji ? '-1.2em' : 0)} `; const Anchor = styled.a` @@ -66,19 +67,31 @@ const Anchor = styled.a` } `; -export const Heading1 = styled(Heading)` +export const StyledHeading = styled(Heading)` position: relative; &:hover { ${Anchor} { visibility: visible; + text-decoration: none; } } `; -export const Heading2 = Heading1.withComponent('h2'); -export const Heading3 = Heading1.withComponent('h3'); -export const Heading4 = Heading1.withComponent('h4'); -export const Heading5 = Heading1.withComponent('h5'); -export const Heading6 = Heading1.withComponent('h6'); - -export default Heading; +export const Heading1 = (props: Props) => ( + +); +export const Heading2 = (props: Props) => ( + +); +export const Heading3 = (props: Props) => ( + +); +export const Heading4 = (props: Props) => ( + +); +export const Heading5 = (props: Props) => ( + +); +export const Heading6 = (props: Props) => ( + +); diff --git a/frontend/components/Editor/components/Toolbar/components/DocumentResult.js b/frontend/components/Editor/components/Toolbar/components/DocumentResult.js index 8351681d6..7d4b157b0 100644 --- a/frontend/components/Editor/components/Toolbar/components/DocumentResult.js +++ b/frontend/components/Editor/components/Toolbar/components/DocumentResult.js @@ -3,7 +3,7 @@ import React from 'react'; import styled from 'styled-components'; import { fontWeight, color } from 'styles/constants'; import Document from 'models/Document'; -import GoToIcon from 'components/Icon/GoToIcon'; +import NextIcon from 'components/Icon/NextIcon'; type Props = { innerRef?: Function, @@ -14,7 +14,7 @@ type Props = { function DocumentResult({ document, ...rest }: Props) { return ( - + {document.title} ); diff --git a/frontend/components/Editor/components/Toolbar/components/LinkToolbar.js b/frontend/components/Editor/components/Toolbar/components/LinkToolbar.js index 010048344..c7b38bb24 100644 --- a/frontend/components/Editor/components/Toolbar/components/LinkToolbar.js +++ b/frontend/components/Editor/components/Toolbar/components/LinkToolbar.js @@ -114,9 +114,10 @@ class LinkToolbar extends Component { const { state } = this.props; const transform = state.transform(); - if (state.selection.isExpanded) { + if (href) { + transform.setInline({ type: 'link', data: { href } }); + } else { transform.unwrapInline('link'); - if (href) transform.wrapInline({ type: 'link', data: { href } }); } this.props.onChange(transform.apply()); @@ -179,7 +180,7 @@ class LinkToolbar extends Component { } const SearchResults = styled.div` - background: rgba(34, 34, 34, .95); + background: #2F3336; position: absolute; top: 100%; width: 100%; diff --git a/frontend/components/Editor/headingToSlug.js b/frontend/components/Editor/headingToSlug.js new file mode 100644 index 000000000..04b9d6273 --- /dev/null +++ b/frontend/components/Editor/headingToSlug.js @@ -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}`); +} diff --git a/frontend/components/Editor/insertImage.js b/frontend/components/Editor/insertImage.js index 7f59ab5ce..d3e4c6bd7 100644 --- a/frontend/components/Editor/insertImage.js +++ b/frontend/components/Editor/insertImage.js @@ -15,6 +15,7 @@ export default async function insertImageFile( try { // load the file as a data URL const id = uuid.v4(); + const alt = ''; const reader = new FileReader(); reader.addEventListener('load', () => { const src = reader.result; @@ -24,7 +25,7 @@ export default async function insertImageFile( .insertBlock({ type: 'image', isVoid: true, - data: { src, id, loading: true }, + data: { src, id, alt, loading: true }, }) .apply(); editor.onChange(state); @@ -45,7 +46,7 @@ export default async function insertImageFile( ); return finalTransform.setNodeByKey(placeholder.key, { - data: { src, loading: false }, + data: { src, alt, loading: false }, }); } catch (err) { throw err; diff --git a/frontend/components/Editor/plugins/MarkdownShortcuts.js b/frontend/components/Editor/plugins/MarkdownShortcuts.js index de530b006..10fad8247 100644 --- a/frontend/components/Editor/plugins/MarkdownShortcuts.js +++ b/frontend/components/Editor/plugins/MarkdownShortcuts.js @@ -73,8 +73,22 @@ export default function MarkdownShortcuts() { let { mark, shortcut } = key; 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++) { - 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); } diff --git a/frontend/components/Editor/types.js b/frontend/components/Editor/types.js index 0db4d68ae..4406f20f9 100644 --- a/frontend/components/Editor/types.js +++ b/frontend/components/Editor/types.js @@ -66,6 +66,7 @@ export type Editor = { export type Node = { key: string, kind: string, + type: string, length: number, text: string, data: Map, diff --git a/frontend/components/Icon/Icon.js b/frontend/components/Icon/Icon.js index 23619a08f..46bf1acbc 100644 --- a/frontend/components/Icon/Icon.js +++ b/frontend/components/Icon/Icon.js @@ -9,6 +9,7 @@ export type Props = { primary?: boolean, color?: string, size?: number, + onClick?: Function, }; type BaseProps = { @@ -18,6 +19,7 @@ type BaseProps = { export default function Icon({ children, className, + onClick, ...rest }: Props & BaseProps) { const size = rest.size ? rest.size + 'px' : '24px'; @@ -36,6 +38,7 @@ export default function Icon({ viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" className={className} + onClick={onClick} > {children} diff --git a/frontend/components/Icon/NextIcon.js b/frontend/components/Icon/NextIcon.js new file mode 100644 index 000000000..ab9144dc6 --- /dev/null +++ b/frontend/components/Icon/NextIcon.js @@ -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 ( + + + + ); +} diff --git a/frontend/components/Layout/components/SidebarCollections.js b/frontend/components/Layout/components/SidebarCollections.js index 0f2242508..7cb3e992a 100644 --- a/frontend/components/Layout/components/SidebarCollections.js +++ b/frontend/components/Layout/components/SidebarCollections.js @@ -182,27 +182,27 @@ const DocumentLink = observer( > 0} - expanded={showChildren} + expand={showChildren} + expandedContent={ + document.children.length + ? + {document.children.map(childDocument => ( + + ))} + + : undefined + } > {document.title} - - {showChildren && - - {document.children && - document.children.map(childDocument => ( - - ))} - } ); } diff --git a/frontend/components/Layout/components/SidebarLink.js b/frontend/components/Layout/components/SidebarLink.js index b3c5456a4..5a349fd47 100644 --- a/frontend/components/Layout/components/SidebarLink.js +++ b/frontend/components/Layout/components/SidebarLink.js @@ -1,5 +1,7 @@ // @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 { color, fontWeight } from 'styles/constants'; import styled from 'styled-components'; @@ -54,22 +56,54 @@ type Props = { onClick?: SyntheticEvent => *, children?: React$Element<*>, icon?: React$Element<*>, - hasChildren?: boolean, - expanded?: boolean, + expand?: boolean, + expandedContent?: React$Element<*>, }; -function SidebarLink({ icon, children, expanded, ...rest }: Props) { - const Component = styleComponent(rest.to ? NavLink : StyleableDiv); +@observer class SidebarLink extends Component { + props: Props; - return ( - - - {icon && {icon}} - {rest.hasChildren && } - {children} - - - ); + componentDidMount() { + if (this.props.expand) this.handleExpand(); + } + + componentDidReceiveProps(nextProps: Props) { + if (nextProps.expand) this.handleExpand(); + } + + @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 ( + + + {icon && {icon}} + {expandedContent && + } + {children} + + {this.expanded && expandedContent} + + ); + } } const Content = styled.div` diff --git a/frontend/index.js b/frontend/index.js index 12e49fe7d..3c5d593ae 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -19,8 +19,7 @@ import 'normalize.css/normalize.css'; import 'styles/base.css'; import 'styles/fonts.css'; import 'styles/transitions.css'; -import 'styles/prism-tomorrow.css'; -import 'styles/hljs-github-gist.css'; +import 'styles/prism.css'; import Home from 'scenes/Home'; import Dashboard from 'scenes/Dashboard'; diff --git a/frontend/scenes/Document/Document.js b/frontend/scenes/Document/Document.js index 371765cd9..ed517e30d 100644 --- a/frontend/scenes/Document/Document.js +++ b/frontend/scenes/Document/Document.js @@ -22,11 +22,11 @@ import Document from 'models/Document'; import DocumentMove from './components/DocumentMove'; import UiStore from 'stores/UiStore'; import DocumentsStore from 'stores/DocumentsStore'; +import CollectionsStore from 'stores/CollectionsStore'; import DocumentMenu from 'menus/DocumentMenu'; import SaveAction from './components/SaveAction'; import LoadingPlaceholder from 'components/LoadingPlaceholder'; import Editor from 'components/Editor'; -import DropToImport from 'components/DropToImport'; import LoadingIndicator from 'components/LoadingIndicator'; import Collaborators from 'components/Collaborators'; import CenteredContent from 'components/CenteredContent'; @@ -45,6 +45,7 @@ type Props = { location: Object, keydown: Object, documents: DocumentsStore, + collections: CollectionsStore, newDocument?: boolean, ui: UiStore, }; @@ -55,7 +56,6 @@ type Props = { @observable editCache: ?string; @observable newDocument: ?Document; - @observable isDragging = false; @observable isLoading = false; @observable isSaving = false; @observable notFound = false; @@ -196,14 +196,6 @@ type Props = { this.props.history.push(url); }; - onStartDragging = () => { - this.isDragging = true; - }; - - onStopDragging = () => { - this.isDragging = false; - }; - renderNotFound() { return ; } @@ -213,7 +205,9 @@ type Props = { const isMoving = this.props.match.path === matchDocumentMove; const document = this.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) { return this.renderNotFound(); @@ -222,11 +216,6 @@ type Props = { return ( {isMoving && document && } - - {this.isDragging && - - Drop files here to import into Atlas. - } {titleText && } {this.isLoading && } {isFetching && @@ -235,73 +224,60 @@ type Props = { } {!isFetching && document && - - - - - - - {!isNew && - !this.isEditing && - } - - {this.isEditing - ? - : - Edit - } - - {this.isEditing && - - Discard - } - {!this.isEditing && - - - } - {!this.isEditing && } - - {!this.isEditing && - - + + + + + + {!isNew && + !this.isEditing && + } + + {this.isEditing + ? + : + Edit } - - - - - } + + {this.isEditing && + + Discard + } + {!this.isEditing && + + + } + {!this.isEditing && } + + {!this.isEditing && + + + } + + + + } ); } @@ -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)` align-items: flex-start; position: fixed; @@ -357,9 +321,6 @@ const LoadingState = styled(LoadingPlaceholder)` margin: 90px 0; `; -const StyledDropToImport = styled(DropToImport)` - display: flex; - flex: 1; -`; - -export default withRouter(inject('ui', 'user', 'documents')(DocumentScene)); +export default withRouter( + inject('ui', 'user', 'documents', 'collections')(DocumentScene) +); diff --git a/frontend/stores/CollectionsStore.js b/frontend/stores/CollectionsStore.js index 6b5d2d997..891206c19 100644 --- a/frontend/stores/CollectionsStore.js +++ b/frontend/stores/CollectionsStore.js @@ -25,6 +25,7 @@ type Options = { type DocumentPathItem = { id: string, title: string, + url: string, type: 'document' | 'collection', }; @@ -59,16 +60,16 @@ class CollectionsStore { let results = []; const travelDocuments = (documentList, path) => documentList.forEach(document => { - const { id, title } = document; - const node = { id, title, type: 'document' }; + const { id, title, url } = document; + const node = { id, title, url, type: 'document' }; results.push(_.concat(path, node)); travelDocuments(document.children, _.concat(path, [node])); }); if (this.isLoaded) { this.data.forEach(collection => { - const { id, name } = collection; - const node = { id, title: name, type: 'collection' }; + const { id, name, url } = collection; + const node = { id, title: name, url, type: 'collection' }; results.push([node]); travelDocuments(collection.documents, [node]); }); @@ -87,6 +88,11 @@ class CollectionsStore { 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 */ @action fetchAll = async (): Promise<*> => { diff --git a/frontend/styles/base.css b/frontend/styles/base.css index 711104aa5..ec8a87f07 100644 --- a/frontend/styles/base.css +++ b/frontend/styles/base.css @@ -101,11 +101,11 @@ samp { } code, samp { - font-size: 87.5%; + font-size: 85%; padding: 0.125em; } pre { - font-size: 87.5%; + font-size: 85%; overflow: scroll; } blockquote { diff --git a/frontend/styles/hljs-github-gist.css b/frontend/styles/hljs-github-gist.css deleted file mode 100644 index 488daf1b8..000000000 --- a/frontend/styles/hljs-github-gist.css +++ /dev/null @@ -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; -} diff --git a/frontend/styles/prism-tomorrow.css b/frontend/styles/prism-tomorrow.css deleted file mode 100644 index 5a67be559..000000000 --- a/frontend/styles/prism-tomorrow.css +++ /dev/null @@ -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; -} diff --git a/frontend/styles/prism.css b/frontend/styles/prism.css new file mode 100644 index 000000000..6a7ff2455 --- /dev/null +++ b/frontend/styles/prism.css @@ -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; +} diff --git a/server/presenters/document.js b/server/presenters/document.js index bf483cd1d..575d18f3b 100644 --- a/server/presenters/document.js +++ b/server/presenters/document.js @@ -14,6 +14,11 @@ async function present(ctx: Object, document: Document, options: ?Options) { ...options, }; 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 = { id: document.id, url: document.getUrl(),