diff --git a/app/actions/definitions/documents.tsx b/app/actions/definitions/documents.tsx index e3d74a1248..68237119d4 100644 --- a/app/actions/definitions/documents.tsx +++ b/app/actions/definitions/documents.tsx @@ -179,12 +179,12 @@ export const unsubscribeDocument = createAction({ }, }); -export const downloadDocument = createAction({ - name: ({ t, isContextMenu }) => - isContextMenu ? t("Download") : t("Download document"), +export const downloadDocumentAsHTML = createAction({ + name: ({ t }) => t("HTML"), section: DocumentSection, + keywords: "html export", icon: , - keywords: "export", + iconInContextMenu: false, visible: ({ activeDocumentId, stores }) => !!activeDocumentId && stores.policies.abilities(activeDocumentId).download, perform: ({ activeDocumentId, stores }) => { @@ -193,10 +193,37 @@ export const downloadDocument = createAction({ } const document = stores.documents.get(activeDocumentId); - document?.download(); + document?.download("text/html"); }, }); +export const downloadDocumentAsMarkdown = createAction({ + name: ({ t }) => t("Markdown"), + section: DocumentSection, + keywords: "md markdown export", + icon: , + iconInContextMenu: false, + visible: ({ activeDocumentId, stores }) => + !!activeDocumentId && stores.policies.abilities(activeDocumentId).download, + perform: ({ activeDocumentId, stores }) => { + if (!activeDocumentId) { + return; + } + + const document = stores.documents.get(activeDocumentId); + document?.download("text/markdown"); + }, +}); + +export const downloadDocument = createAction({ + name: ({ t, isContextMenu }) => + isContextMenu ? t("Download") : t("Download document"), + section: DocumentSection, + icon: , + keywords: "export", + children: [downloadDocumentAsHTML, downloadDocumentAsMarkdown], +}); + export const duplicateDocument = createAction({ name: ({ t, isContextMenu }) => isContextMenu ? t("Duplicate") : t("Duplicate document"), diff --git a/app/components/Theme.tsx b/app/components/Theme.tsx index d71b1c9d7a..19bb85a85e 100644 --- a/app/components/Theme.tsx +++ b/app/components/Theme.tsx @@ -2,10 +2,10 @@ import { observer } from "mobx-react"; import * as React from "react"; import { ThemeProvider } from "styled-components"; import { breakpoints } from "@shared/styles"; +import GlobalStyles from "@shared/styles/globals"; import { dark, light, lightMobile, darkMobile } from "@shared/styles/theme"; import useMediaQuery from "~/hooks/useMediaQuery"; import useStores from "~/hooks/useStores"; -import GlobalStyles from "~/styles/globals"; const Theme: React.FC = ({ children }) => { const { ui } = useStores(); diff --git a/app/editor/components/Styles.ts b/app/editor/components/Styles.ts deleted file mode 100644 index 7cbeac823f..0000000000 --- a/app/editor/components/Styles.ts +++ /dev/null @@ -1,1304 +0,0 @@ -/* eslint-disable no-irregular-whitespace */ -import { darken, lighten, transparentize } from "polished"; -import styled from "styled-components"; -import { depths } from "@shared/styles"; - -const EditorStyles = styled.div<{ - rtl: boolean; - readOnly?: boolean; - readOnlyWriteCheckboxes?: boolean; - grow?: boolean; -}>` - flex-grow: ${(props) => (props.grow ? 1 : 0)}; - justify-content: start; - color: ${(props) => props.theme.text}; - font-family: ${(props) => props.theme.fontFamily}; - font-weight: ${(props) => props.theme.fontWeight}; - font-size: 1em; - line-height: 1.6em; - width: 100%; - - > div { - background: transparent; - } - - & * { - box-sizing: content-box; - } - - .ProseMirror { - position: relative; - outline: none; - word-wrap: break-word; - white-space: pre-wrap; - white-space: break-spaces; - -webkit-font-variant-ligatures: none; - font-variant-ligatures: none; - font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */ - - & > .ProseMirror-yjs-cursor { - display: none; - } - - & > * { - margin-top: .5em; - margin-bottom: .5em; - } - - & > :first-child, - & > button:first-child + * { - margin-top: 0; - } - - h2, - h3, - h4, - h5, - h6 { - margin-top: 1em; - } - - h1 { - margin-top: .75em; - margin-bottom: 0.25em; - } - - // all of heading sizes are stepped down one from global styles, except h1 - // which is between h1 and h2 - h1 { font-size: 1.75em; } - h2 { font-size: 1.25em; } - h3 { font-size: 1em; } - h4 { font-size: 0.875em; } - h5 { font-size: 0.75em; } - h6 { font-size: 0.75em; } - - .ProseMirror-yjs-cursor { - position: relative; - margin-left: -1px; - margin-right: -1px; - border-left: 1px solid black; - border-right: 1px solid black; - height: 1em; - word-break: normal; - - &:after { - content: ""; - display: block; - position: absolute; - left: -8px; - right: -8px; - top: 0; - bottom: 0; - } - > div { - opacity: 0; - transition: opacity 100ms ease-in-out; - position: absolute; - top: -1.8em; - font-size: 13px; - background-color: rgb(250, 129, 0); - font-style: normal; - line-height: normal; - user-select: none; - white-space: nowrap; - color: white; - padding: 2px 6px; - font-weight: 500; - border-radius: 4px; - pointer-events: none; - left: -1px; - } - - &:hover { - > div { - opacity: 1; - } - } - } - } - - &.show-cursor-names .ProseMirror-yjs-cursor > div { - opacity: 1; - } - - pre { - white-space: pre-wrap; - } - - li { - position: relative; - } - - .image { - line-height: 0; - text-align: center; - max-width: 100%; - clear: both; - - img { - pointer-events: ${(props) => (props.readOnly ? "initial" : "none")}; - display: inline-block; - max-width: 100%; - max-height: 75vh; - } - - .ProseMirror-selectednode img { - pointer-events: initial; - } - } - - .image.placeholder { - position: relative; - background: ${(props) => props.theme.background}; - margin-bottom: calc(28px + 1.2em); - - img { - opacity: 0.5; - } - } - - .image-replacement-uploading { - img { - opacity: 0.5; - } - } - - .image-right-50 { - float: right; - width: 50%; - margin-left: 2em; - margin-bottom: 1em; - clear: initial; - } - - .image-left-50 { - float: left; - width: 50%; - margin-right: 2em; - margin-bottom: 1em; - clear: initial; - } - - .ProseMirror-hideselection *::selection { - background: transparent; - } - .ProseMirror-hideselection *::-moz-selection { - background: transparent; - } - .ProseMirror-hideselection { - caret-color: transparent; - } - - .ProseMirror-selectednode { - outline: 2px solid - ${(props) => (props.readOnly ? "transparent" : props.theme.selected)}; - } - - /* Make sure li selections wrap around markers */ - - li.ProseMirror-selectednode { - outline: none; - } - - li.ProseMirror-selectednode:after { - content: ""; - position: absolute; - left: ${(props) => (props.rtl ? "-2px" : "-32px")}; - right: ${(props) => (props.rtl ? "-32px" : "-2px")}; - top: -2px; - bottom: -2px; - border: 2px solid ${(props) => props.theme.selected}; - pointer-events: none; - } - - img.ProseMirror-separator { - display: inline; - border: none !important; - margin: 0 !important; - } - - // Removes forced paragraph spaces below images, this is needed to images - // being inline nodes that are displayed like blocks - .component-image + img.ProseMirror-separator, - .component-image + img.ProseMirror-separator + br.ProseMirror-trailingBreak { - display: none; - } - - .ProseMirror[contenteditable="false"] { - .caption { - pointer-events: none; - } - .caption:empty { - visibility: hidden; - } - } - - h1, - h2, - h3, - h4, - h5, - h6 { - font-weight: 500; - cursor: text; - - &:not(.placeholder):before { - display: ${(props) => (props.readOnly ? "none" : "inline-block")}; - font-family: ${(props) => props.theme.fontFamilyMono}; - color: ${(props) => props.theme.textSecondary}; - font-size: 13px; - line-height: 0; - margin-${(props) => (props.rtl ? "right" : "left")}: -24px; - transition: opacity 150ms ease-in-out; - opacity: 0; - width: 24px; - } - - &:hover, - &:focus-within { - .heading-actions { - opacity: 1; - } - } - } - - .heading-content { - &:before { - content: "​"; - display: inline; - } - } - - .heading-name { - color: ${(props) => props.theme.text}; - pointer-events: none; - display: block; - position: relative; - top: -60px; - visibility: hidden; - - &:hover { - text-decoration: none; - } - } - - .heading-name:first-child, - .heading-name:first-child + .ProseMirror-yjs-cursor { - & + h1, - & + h2, - & + h3, - & + h4 { - margin-top: 0; - } - } - - a:first-child { - h1, - h2, - h3, - h4, - h5, - h6 { - margin-top: 0; - } - } - - h1:not(.placeholder):before { - content: "H1"; - } - h2:not(.placeholder):before { - content: "H2"; - } - h3:not(.placeholder):before { - content: "H3"; - } - h4:not(.placeholder):before { - content: "H4"; - } - h5:not(.placeholder):before { - content: "H5"; - } - h6:not(.placeholder):before { - content: "H6"; - } - - .ProseMirror-focused { - h1, - h2, - h3, - h4, - h5, - h6 { - &:not(.placeholder):before { - opacity: 1; - } - } - } - - .with-emoji { - margin-${(props) => (props.rtl ? "right" : "left")}: -1em; - } - - .heading-anchor, - .heading-fold { - display: inline-block; - color: ${(props) => props.theme.text}; - opacity: .75; - cursor: pointer; - background: none; - outline: none; - border: 0; - margin: 0; - padding: 0; - text-align: left; - font-family: ${(props) => props.theme.fontFamilyMono}; - font-size: 14px; - line-height: 0; - width: 12px; - height: 24px; - - &:focus, - &:hover { - opacity: 1; - } - } - - .heading-anchor { - box-sizing: border-box; - } - - .heading-actions { - opacity: 0; - z-index: ${depths.editorHeadingActions}; - background: ${(props) => props.theme.background}; - margin-${(props) => (props.rtl ? "right" : "left")}: -26px; - flex-direction: ${(props) => (props.rtl ? "row-reverse" : "row")}; - display: inline-flex; - position: relative; - top: -2px; - width: 26px; - height: 24px; - - &.collapsed { - opacity: 1; - } - - &.collapsed .heading-anchor { - opacity: 0; - } - - &.collapsed .heading-fold { - opacity: 1; - } - } - - h1, - h2, - h3, - h4, - h5, - h6 { - &:hover { - .heading-anchor { - opacity: 0.75 !important; - } - .heading-anchor:hover { - opacity: 1 !important; - } - } - } - - .heading-fold { - display: inline-block; - transform-origin: center; - padding: 0; - - &.collapsed { - svg { - transform: rotate(${(props) => (props.rtl ? "90deg" : "-90deg")}); - pointer-events: none; - } - transition-delay: 0.1s; - opacity: 1; - } - } - - .placeholder:before { - display: block; - opacity: 0; - transition: opacity 150ms ease-in-out; - content: ${(props) => (props.readOnly ? "" : "attr(data-empty-text)")}; - pointer-events: none; - height: 0; - color: ${(props) => props.theme.placeholder}; - } - - /** Show the placeholder if focused or the first visible item nth(2) accounts for block insert trigger */ - .ProseMirror-focused .placeholder:before, - .placeholder:nth-child(1):before, - .placeholder:nth-child(2):before { - opacity: 1; - } - - .notice-block { - display: flex; - align-items: center; - background: ${(props) => - transparentize(0.9, props.theme.noticeInfoBackground)}; - border-left: 4px solid ${(props) => props.theme.noticeInfoBackground}; - color: ${(props) => props.theme.noticeInfoText}; - border-radius: 4px; - padding: 8px 10px 8px 8px; - margin: 8px 0; - - a { - color: ${(props) => props.theme.noticeInfoText}; - } - - a:not(.heading-name) { - text-decoration: underline; - } - - p:first-child { - margin-top: 0; - } - - p:last-child { - margin-bottom: 0; - } - } - - .notice-block .content { - flex-grow: 1; - min-width: 0; - } - - .notice-block .icon { - width: 24px; - height: 24px; - align-self: flex-start; - margin-${(props) => (props.rtl ? "left" : "right")}: 4px; - color: ${(props) => props.theme.noticeInfoBackground}; - } - - .notice-block.tip { - background: ${(props) => - transparentize(0.9, props.theme.noticeTipBackground)}; - border-left: 4px solid ${(props) => props.theme.noticeTipBackground}; - color: ${(props) => props.theme.noticeTipText}; - - .icon { - color: ${(props) => props.theme.noticeTipBackground}; - } - - a { - color: ${(props) => props.theme.noticeTipText}; - } - } - - .notice-block.warning { - background: ${(props) => - transparentize(0.9, props.theme.noticeWarningBackground)}; - border-left: 4px solid ${(props) => props.theme.noticeWarningBackground}; - color: ${(props) => props.theme.noticeWarningText}; - - .icon { - color: ${(props) => props.theme.noticeWarningBackground}; - } - - a { - color: ${(props) => props.theme.noticeWarningText}; - } - } - - blockquote { - margin: 0; - padding-left: 1.5em; - font-style: italic; - overflow: hidden; - position: relative; - - &:before { - content: ""; - display: inline-block; - width: 2px; - border-radius: 1px; - position: absolute; - margin-${(props) => (props.rtl ? "right" : "left")}: -1.5em; - top: 0; - bottom: 0; - background: ${(props) => props.theme.quote}; - } - } - - b, - strong { - font-weight: 600; - } - - .template-placeholder { - color: ${(props) => props.theme.placeholder}; - border-bottom: 1px dotted ${(props) => props.theme.placeholder}; - border-radius: 2px; - cursor: text; - - &:hover { - border-bottom: 1px dotted - ${(props) => - props.readOnly ? props.theme.placeholder : props.theme.textSecondary}; - } - } - - p { - margin: 0; - - span:first-child + br:last-child { - display: none; - } - - a { - color: ${(props) => props.theme.text}; - text-decoration: underline; - text-decoration-color: ${(props) => lighten(0.5, props.theme.text)}; - text-decoration-thickness: 1px; - text-underline-offset: .15em; - font-weight: 500; - - &:hover { - text-decoration: underline; - text-decoration-color: ${(props) => props.theme.text}; - text-decoration-thickness: 1px; - } - } - } - - a { - color: ${(props) => props.theme.link}; - cursor: pointer; - } - - .ProseMirror-focused { - a { - cursor: text; - } - } - - a:hover { - text-decoration: ${(props) => (props.readOnly ? "underline" : "none")}; - } - - ul, - ol { - margin: ${(props) => (props.rtl ? "0 -26px 0 0.1em" : "0 0.1em 0 -26px")}; - padding: ${(props) => (props.rtl ? "0 44px 0 0" : "0 0 0 44px")}; - } - - ol ol { - list-style: lower-alpha; - } - - ol ol ol { - list-style: lower-roman; - } - - ul.checkbox_list { - list-style: none; - padding: 0; - margin-left: ${(props) => (props.rtl ? "0" : "-24px")}; - margin-right: ${(props) => (props.rtl ? "-24px" : "0")}; - } - - ul li, - ol li { - position: relative; - white-space: initial; - - p { - white-space: pre-wrap; - } - - > div { - width: 100%; - } - } - - ul.checkbox_list li { - display: flex; - padding-${(props) => (props.rtl ? "right" : "left")}: 24px; - } - - ul.checkbox_list li.checked > div > p { - color: ${(props) => props.theme.textSecondary}; - text-decoration: line-through; - } - - ul li::before, - ol li::before { - background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iOCIgeT0iNyIgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIGZpbGw9IiM0RTVDNkUiLz4KPHJlY3QgeD0iOCIgeT0iMTEiIHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiBmaWxsPSIjNEU1QzZFIi8+CjxyZWN0IHg9IjgiIHk9IjE1IiB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgZmlsbD0iIzRFNUM2RSIvPgo8cmVjdCB4PSIxMyIgeT0iNyIgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIGZpbGw9IiM0RTVDNkUiLz4KPHJlY3QgeD0iMTMiIHk9IjExIiB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgZmlsbD0iIzRFNUM2RSIvPgo8cmVjdCB4PSIxMyIgeT0iMTUiIHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiBmaWxsPSIjNEU1QzZFIi8+Cjwvc3ZnPgo=") no-repeat; - background-position: 0 2px; - content: ""; - display: ${(props) => (props.readOnly ? "none" : "inline-block")}; - cursor: grab; - width: 24px; - height: 24px; - position: absolute; - ${(props) => (props.rtl ? "right" : "left")}: -40px; - opacity: 0; - transition: opacity 200ms ease-in-out; - } - - ul li[draggable=true]::before, - ol li[draggable=true]::before { - cursor: grabbing; - } - - ul > li.counter-2::before, - ol li.counter-2::before { - ${(props) => (props.rtl ? "right" : "left")}: -50px; - } - - ul > li.hovering::before, - ol li.hovering::before { - opacity: 0.5; - } - - ul li.ProseMirror-selectednode::after, - ol li.ProseMirror-selectednode::after { - display: none; - } - - ul.checkbox_list li::before { - ${(props) => (props.rtl ? "right" : "left")}: 0; - } - - ul.checkbox_list li .checkbox { - display: inline-block; - cursor: pointer; - pointer-events: ${(props) => - props.readOnly && !props.readOnlyWriteCheckboxes ? "none" : "initial"}; - opacity: ${(props) => - props.readOnly && !props.readOnlyWriteCheckboxes ? 0.75 : 1}; - margin: ${(props) => (props.rtl ? "0 0 0 0.5em" : "0 0.5em 0 0")}; - width: 14px; - height: 14px; - position: relative; - top: 1px; - transition: transform 100ms ease-in-out; - opacity: .8; - - background-image: ${(props) => - `url("data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3 0C1.34315 0 0 1.34315 0 3V11C0 12.6569 1.34315 14 3 14H11C12.6569 14 14 12.6569 14 11V3C14 1.34315 12.6569 0 11 0H3ZM3 2C2.44772 2 2 2.44772 2 3V11C2 11.5523 2.44772 12 3 12H11C11.5523 12 12 11.5523 12 11V3C12 2.44772 11.5523 2 11 2H3Z' fill='${props.theme.text.replace( - "#", - "%23" - )}' /%3E%3C/svg%3E%0A");`} - - &[aria-checked=true] { - opacity: 1; - background-image: ${(props) => - `url( - "data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3 0C1.34315 0 0 1.34315 0 3V11C0 12.6569 1.34315 14 3 14H11C12.6569 14 14 12.6569 14 11V3C14 1.34315 12.6569 0 11 0H3ZM4.26825 5.85982L5.95873 7.88839L9.70003 2.9C10.0314 2.45817 10.6582 2.36863 11.1 2.7C11.5419 3.03137 11.6314 3.65817 11.3 4.1L6.80002 10.1C6.41275 10.6164 5.64501 10.636 5.2318 10.1402L2.7318 7.14018C2.37824 6.71591 2.43556 6.08534 2.85984 5.73178C3.28412 5.37821 3.91468 5.43554 4.26825 5.85982Z' fill='${props.theme.primary.replace( - "#", - "%23" - )}' /%3E%3C/svg%3E%0A" - )`}; - } - - &:active { - transform: scale(0.9); - } - } - - li p:first-child { - margin: 0; - word-break: break-word; - } - - hr { - position: relative; - height: 1em; - border: 0; - } - - hr:before { - content: ""; - display: block; - position: absolute; - border-top: 1px solid ${(props) => props.theme.horizontalRule}; - top: 0.5em; - left: 0; - right: 0; - } - - hr.page-break { - page-break-after: always; - } - - hr.page-break:before { - border-top: 1px dashed ${(props) => props.theme.horizontalRule}; - } - - code { - border-radius: 4px; - border: 1px solid ${(props) => props.theme.codeBorder}; - background: ${(props) => props.theme.codeBackground}; - padding: 3px 4px; - font-family: ${(props) => props.theme.fontFamilyMono}; - font-size: 80%; - } - - mark { - border-radius: 1px; - color: ${(props) => props.theme.textHighlightForeground}; - background: ${(props) => props.theme.textHighlight}; - - a { - color: ${(props) => props.theme.textHighlightForeground}; - } - } - - .external-link { - display: inline-block; - position: relative; - top: 2px; - width: 16px; - height: 16px; - } - - .code-actions, - .notice-actions { - display: flex; - align-items: center; - gap: 8px; - position: absolute; - z-index: 1; - top: 8px; - right: 8px; - } - - .notice-actions { - ${(props) => (props.rtl ? "left" : "right")}: 8px; - } - - .code-block, - .notice-block { - position: relative; - - select, - button { - margin: 0; - padding: 0; - border: 0; - background: ${(props) => props.theme.buttonNeutralBackground}; - color: ${(props) => props.theme.buttonNeutralText}; - box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${(props) => - props.theme.buttonNeutralBorder} 0 0 0 1px inset; - border-radius: 4px; - font-size: 13px; - font-weight: 500; - text-decoration: none; - flex-shrink: 0; - cursor: pointer; - user-select: none; - appearance: none !important; - padding: 6px 8px; - display: none; - - &::-moz-focus-inner { - padding: 0; - border: 0; - } - - &:hover:not(:disabled) { - background-color: ${(props) => - darken(0.05, props.theme.buttonNeutralBackground)}; - box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${(props) => - props.theme.buttonNeutralBorder} 0 0 0 1px inset; - } - } - - select { - background-image: url('data:image/svg+xml;utf8, '); - background-repeat: no-repeat; - background-position: center right; - padding-right: 20px; - } - - &:focus-within, - &:hover { - select { - display: ${(props) => (props.readOnly ? "none" : "inline")}; - } - - button { - display: inline; - } - } - - select:focus, - select:active, - button:focus, - button:active { - display: inline; - } - - button.show-source-button { - display: none; - } - button.show-diagram-button { - display: inline; - } - - &.code-hidden { - button, - select, - button.show-diagram-button { - display: none; - } - - button.show-source-button { - display: inline; - } - - pre { - display: none; - } - } - } - - .mermaid-diagram-wrapper { - display: flex; - align-items: center; - justify-content: center; - background: ${(props) => props.theme.codeBackground}; - border-radius: 6px; - border: 1px solid ${(props) => props.theme.codeBorder}; - padding: 8px; - user-select: none; - cursor: default; - - * { - font-family: ${(props) => props.theme.fontFamily}; - } - - &.diagram-hidden { - display: none; - } - } - - pre { - display: block; - overflow-x: auto; - padding: 0.75em 1em; - line-height: 1.4em; - position: relative; - background: ${(props) => props.theme.codeBackground}; - border-radius: 4px; - border: 1px solid ${(props) => props.theme.codeBorder}; - margin: .5em 0; - - -webkit-font-smoothing: initial; - font-family: ${(props) => props.theme.fontFamilyMono}; - font-size: 13px; - 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: ${(props) => props.theme.code}; - - code { - font-size: 13px; - background: none; - padding: 0; - border: 0; - } - } - - .token.comment, - .token.prolog, - .token.doctype, - .token.cdata { - color: ${(props) => props.theme.codeComment}; - } - - .token.punctuation { - color: ${(props) => props.theme.codePunctuation}; - } - - .token.namespace { - opacity: 0.7; - } - - .token.operator, - .token.boolean, - .token.number { - color: ${(props) => props.theme.codeNumber}; - } - - .token.property { - color: ${(props) => props.theme.codeProperty}; - } - - .token.tag { - color: ${(props) => props.theme.codeTag}; - } - - .token.string { - color: ${(props) => props.theme.codeString}; - } - - .token.selector { - color: ${(props) => props.theme.codeSelector}; - } - - .token.attr-name { - color: ${(props) => props.theme.codeAttr}; - } - - .token.entity, - .token.url, - .language-css .token.string, - .style .token.string { - color: ${(props) => props.theme.codeEntity}; - } - - .token.attr-value, - .token.keyword, - .token.control, - .token.directive, - .token.unit { - color: ${(props) => props.theme.codeKeyword}; - } - - .token.function { - color: ${(props) => props.theme.codeFunction}; - } - - .token.statement, - .token.regex, - .token.atrule { - color: ${(props) => props.theme.codeStatement}; - } - - .token.placeholder, - .token.variable { - color: ${(props) => props.theme.codePlaceholder}; - } - - .token.deleted { - text-decoration: line-through; - } - - .token.inserted { - border-bottom: 1px dotted ${(props) => props.theme.codeInserted}; - text-decoration: none; - } - - .token.italic { - font-style: italic; - } - - .token.important, - .token.bold { - font-weight: bold; - } - - .token.important { - color: ${(props) => props.theme.codeImportant}; - } - - .token.entity { - cursor: help; - } - - table { - width: 100%; - border-collapse: collapse; - border-radius: 4px; - margin-top: 1em; - box-sizing: border-box; - - * { - box-sizing: border-box; - } - - tr { - position: relative; - border-bottom: 1px solid ${(props) => props.theme.tableDivider}; - } - - th { - background: ${(props) => props.theme.tableHeaderBackground}; - } - - td, - th { - position: relative; - vertical-align: top; - border: 1px solid ${(props) => props.theme.tableDivider}; - position: relative; - padding: 4px 8px; - text-align: ${(props) => (props.rtl ? "right" : "left")}; - min-width: 100px; - } - - .selectedCell { - background: ${(props) => - props.readOnly ? "inherit" : props.theme.tableSelectedBackground}; - - /* fixes Firefox background color painting over border: - * https://bugzilla.mozilla.org/show_bug.cgi?id=688556 */ - background-clip: padding-box; - } - - .grip-column { - /* usage of ::after for all of the table grips works around a bug in - * prosemirror-tables that causes Safari to hang when selecting a cell - * in an empty table: - * https://github.com/ProseMirror/prosemirror/issues/947 */ - &::after { - content: ""; - cursor: pointer; - position: absolute; - top: -16px; - ${(props) => (props.rtl ? "right" : "left")}: 0; - width: 100%; - height: 12px; - background: ${(props) => props.theme.tableDivider}; - border-bottom: 3px solid ${(props) => props.theme.background}; - display: ${(props) => (props.readOnly ? "none" : "block")}; - } - - &:hover::after { - background: ${(props) => props.theme.text}; - } - &.first::after { - border-top-${(props) => (props.rtl ? "right" : "left")}-radius: 3px; - } - &.last::after { - border-top-${(props) => (props.rtl ? "left" : "right")}-radius: 3px; - } - &.selected::after { - background: ${(props) => props.theme.tableSelected}; - } - } - - .grip-row { - &::after { - content: ""; - cursor: pointer; - position: absolute; - ${(props) => (props.rtl ? "right" : "left")}: -16px; - top: 0; - height: 100%; - width: 12px; - background: ${(props) => props.theme.tableDivider}; - border-${(props) => (props.rtl ? "left" : "right")}: 3px solid; - border-color: ${(props) => props.theme.background}; - display: ${(props) => (props.readOnly ? "none" : "block")}; - } - - &:hover::after { - background: ${(props) => props.theme.text}; - } - &.first::after { - border-top-${(props) => (props.rtl ? "right" : "left")}-radius: 3px; - } - &.last::after { - border-bottom-${(props) => (props.rtl ? "right" : "left")}-radius: 3px; - } - &.selected::after { - background: ${(props) => props.theme.tableSelected}; - } - } - - .grip-table { - &::after { - content: ""; - cursor: pointer; - background: ${(props) => props.theme.tableDivider}; - width: 13px; - height: 13px; - border-radius: 13px; - border: 2px solid ${(props) => props.theme.background}; - position: absolute; - top: -18px; - ${(props) => (props.rtl ? "right" : "left")}: -18px; - display: ${(props) => (props.readOnly ? "none" : "block")}; - } - - &:hover::after { - background: ${(props) => props.theme.text}; - } - &.selected::after { - background: ${(props) => props.theme.tableSelected}; - } - } - } - - .scrollable-wrapper { - position: relative; - margin: 0.5em 0px; - scrollbar-width: thin; - scrollbar-color: transparent transparent; - - &:hover { - scrollbar-color: ${(props) => props.theme.scrollbarThumb} ${(props) => - props.theme.scrollbarBackground}; - } - - & ::-webkit-scrollbar { - height: 14px; - background-color: transparent; - } - - &:hover ::-webkit-scrollbar { - background-color: ${(props) => props.theme.scrollbarBackground}; - } - - & ::-webkit-scrollbar-thumb { - background-color: transparent; - border: 3px solid transparent; - border-radius: 7px; - } - - &:hover ::-webkit-scrollbar-thumb { - background-color: ${(props) => props.theme.scrollbarThumb}; - border-color: ${(props) => props.theme.scrollbarBackground}; - } - } - - .scrollable { - overflow-y: hidden; - overflow-x: auto; - padding-${(props) => (props.rtl ? "right" : "left")}: 1em; - margin-${(props) => (props.rtl ? "right" : "left")}: -1em; - border-${(props) => (props.rtl ? "right" : "left")}: 1px solid transparent; - border-${(props) => (props.rtl ? "left" : "right")}: 1px solid transparent; - transition: border 250ms ease-in-out 0s; - } - - .scrollable-shadow { - position: absolute; - top: 0; - bottom: 0; - ${(props) => (props.rtl ? "right" : "left")}: -1em; - width: 16px; - transition: box-shadow 250ms ease-in-out; - border: 0px solid transparent; - border-${(props) => (props.rtl ? "right" : "left")}-width: 1em; - pointer-events: none; - - &.left { - box-shadow: 16px 0 16px -16px inset rgba(0, 0, 0, 0.25); - border-left: 1em solid ${(props) => props.theme.background}; - } - - &.right { - right: 0; - left: auto; - box-shadow: -16px 0 16px -16px inset rgba(0, 0, 0, 0.25); - } - } - - .block-menu-trigger { - opacity: 0; - pointer-events: none; - display: ${(props) => (props.readOnly ? "none" : "inline")}; - width: 24px; - height: 24px; - color: ${(props) => props.theme.textSecondary}; - background: none; - position: absolute; - transition: color 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275), - opacity 150ms ease-in-out; - outline: none; - border: 0; - padding: 0; - margin-top: 1px; - margin-${(props) => (props.rtl ? "right" : "left")}: -28px; - border-radius: 4px; - - &:hover, - &:focus { - cursor: pointer; - color: ${(props) => props.theme.text}; - background: ${(props) => props.theme.secondaryBackground}; - } - } - - .ProseMirror-focused .block-menu-trigger, - .block-menu-trigger:active, - .block-menu-trigger:focus { - opacity: 1; - pointer-events: initial; - } - - .ProseMirror-gapcursor { - display: none; - pointer-events: none; - position: absolute; - } - - .ProseMirror-gapcursor:after { - content: ""; - display: block; - position: absolute; - top: -2px; - width: 20px; - border-top: 1px solid ${(props) => props.theme.cursor}; - animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; - } - - .folded-content { - display: none; - } - - @keyframes ProseMirror-cursor-blink { - to { - visibility: hidden; - } - } - - .ProseMirror-focused .ProseMirror-gapcursor { - display: block; - } - - @media print { - .placeholder:before, - .block-menu-trigger, - .heading-actions, - button.show-source-button, - h1:not(.placeholder):before, - h2:not(.placeholder):before, - h3:not(.placeholder):before, - h4:not(.placeholder):before, - h5:not(.placeholder):before, - h6:not(.placeholder):before { - display: none; - } - - .page-break { - opacity: 0; - } - - pre { - overflow-x: hidden; - white-space: pre-wrap; - } - - em, - blockquote { - font-family: "SF Pro Text", ${(props) => props.theme.fontFamily}; - } - } -`; - -export default EditorStyles; diff --git a/app/editor/index.tsx b/app/editor/index.tsx index e448e30e83..8713764abf 100644 --- a/app/editor/index.tsx +++ b/app/editor/index.tsx @@ -16,6 +16,7 @@ import { EditorState, Selection, Plugin, Transaction } from "prosemirror-state"; import { Decoration, EditorView } from "prosemirror-view"; import * as React from "react"; import { DefaultTheme, ThemeProps } from "styled-components"; +import EditorContainer from "@shared/editor/components/Styles"; import { EmbedDescriptor } from "@shared/editor/embeds"; import Extension, { CommandFactory } from "@shared/editor/lib/Extension"; import ExtensionManager from "@shared/editor/lib/ExtensionManager"; @@ -40,7 +41,6 @@ import EmojiMenu from "./components/EmojiMenu"; import { SearchResult } from "./components/LinkEditor"; import LinkToolbar from "./components/LinkToolbar"; import SelectionToolbar from "./components/SelectionToolbar"; -import EditorContainer from "./components/Styles"; import WithTheme from "./components/WithTheme"; export { default as Extension } from "@shared/editor/lib/Extension"; diff --git a/app/menus/DocumentMenu.tsx b/app/menus/DocumentMenu.tsx index 9335a9a611..8efc650b43 100644 --- a/app/menus/DocumentMenu.tsx +++ b/app/menus/DocumentMenu.tsx @@ -296,6 +296,7 @@ function DocumentMenu({ { type: "separator", }, + actionToMenuItem(downloadDocument, context), { type: "route", title: t("History"), @@ -305,7 +306,6 @@ function DocumentMenu({ visible: canViewHistory, icon: , }, - actionToMenuItem(downloadDocument, context), { type: "button", title: t("Print"), diff --git a/app/models/Document.ts b/app/models/Document.ts index 60cddc471e..323556b203 100644 --- a/app/models/Document.ts +++ b/app/models/Document.ts @@ -2,10 +2,10 @@ import { addDays, differenceInDays } from "date-fns"; import { floor } from "lodash"; import { action, autorun, computed, observable, set } from "mobx"; import parseTitle from "@shared/utils/parseTitle"; -import unescape from "@shared/utils/unescape"; import DocumentsStore from "~/stores/DocumentsStore"; import User from "~/models/User"; import type { NavigationNode } from "~/types"; +import { client } from "~/utils/ApiClient"; import Storage from "~/utils/Storage"; import ParanoidModel from "./ParanoidModel"; import View from "./View"; @@ -419,21 +419,18 @@ export default class Document extends ParanoidModel { }; } - download = async () => { - // Ensure the document is upto date with latest server contents - await this.fetch(); - const body = unescape(this.text); - const blob = new Blob([`# ${this.title}\n\n${body}`], { - type: "text/markdown", - }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - // Firefox support requires the anchor tag be in the DOM to trigger the dl - if (document.body) { - document.body.appendChild(a); - } - a.href = url; - a.download = `${this.titleWithDefault}.md`; - a.click(); + download = async (contentType: "text/html" | "text/markdown") => { + await client.post( + `/documents.export`, + { + id: this.id, + }, + { + download: true, + headers: { + accept: contentType, + }, + } + ); }; } diff --git a/app/utils/ApiClient.ts b/app/utils/ApiClient.ts index f5d9e8625b..d4f2635ca4 100644 --- a/app/utils/ApiClient.ts +++ b/app/utils/ApiClient.ts @@ -25,6 +25,7 @@ type Options = { type FetchOptions = { download?: boolean; + headers?: Record; }; const fetchWithRetry = retry(fetch); @@ -81,6 +82,7 @@ class ApiClient { "cache-control": "no-cache", "x-editor-version": EDITOR_VERSION, pragma: "no-cache", + ...options?.headers, }; // for multipart forms or other non JSON requests fetch diff --git a/app/utils/download.ts b/app/utils/download.ts index 94d4a57fe8..89ba73c568 100644 --- a/app/utils/download.ts +++ b/app/utils/download.ts @@ -67,7 +67,6 @@ export default function download( if ("download" in a) { a.href = url; a.setAttribute("download", fn); - a.innerHTML = "downloading…"; D.body && D.body.appendChild(a); setTimeout(function () { a.click(); diff --git a/package.json b/package.json index 10f3071cd1..7367fb9a20 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server", "start": "node ./build/server/index.js", "dev": "NODE_ENV=development yarn concurrently -n api,collaboration -c \"blue,magenta\" \"node --inspect=0.0.0.0 build/server/index.js --services=collaboration,websockets,admin,web,worker\"", - "dev:watch": "nodemon --exec \"yarn build:server && yarn dev\" -e js,ts --ignore build/ --ignore app/ --ignore shared/editor", + "dev:watch": "nodemon --exec \"yarn build:server && yarn dev\" -e js,ts,tsx --ignore build/ --ignore app/ --ignore shared/editor", "lint": "eslint app server shared", "deploy": "git push heroku master", "prepare": "husky install", @@ -108,6 +108,7 @@ "invariant": "^2.2.4", "ioredis": "^4.28.5", "is-printable-key-event": "^1.0.0", + "jsdom": "^20.0.0", "json-loader": "0.5.4", "jsonwebtoken": "^8.5.0", "jszip": "^3.10.0", diff --git a/server/commands/documentLoader.ts b/server/commands/documentLoader.ts index 8a3767a788..5618a3cc1f 100644 --- a/server/commands/documentLoader.ts +++ b/server/commands/documentLoader.ts @@ -13,6 +13,7 @@ type Props = { id?: string; shareId?: string; user?: User; + includeState?: boolean; }; type Result = { @@ -25,6 +26,7 @@ export default async function loadDocument({ id, shareId, user, + includeState, }: Props): Promise { let document; let collection; @@ -156,6 +158,7 @@ export default async function loadDocument({ document = await Document.findByPk(id as string, { userId: user ? user.id : undefined, paranoid: false, + includeState, }); if (!document) { diff --git a/server/commands/documentUpdater.ts b/server/commands/documentUpdater.ts index 61d65ecc7c..a494206861 100644 --- a/server/commands/documentUpdater.ts +++ b/server/commands/documentUpdater.ts @@ -1,5 +1,6 @@ import { Transaction } from "sequelize"; import { Event, Document, User } from "@server/models"; +import DocumentHelper from "@server/models/helpers/DocumentHelper"; type Props = { /** The user updating the document */ @@ -62,7 +63,7 @@ export default async function documentUpdater({ } if (text !== undefined) { if (user.team?.collaborativeEditing) { - document.updateFromMarkdown(text, append); + document = DocumentHelper.applyMarkdownToDocument(document, text, append); } else if (append) { document.text += text; } else { diff --git a/server/editor/__snapshots__/renderToHtml.test.ts.snap b/server/editor/__snapshots__/renderToHtml.test.ts.snap deleted file mode 100644 index 8195396f17..0000000000 --- a/server/editor/__snapshots__/renderToHtml.test.ts.snap +++ /dev/null @@ -1,129 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`renders blockquote 1`] = ` -"
-

blockquote

-
" -`; - -exports[`renders bold marks 1`] = `"

this is bold text

"`; - -exports[`renders bullet list 1`] = ` -"
    -
  • item one
  • -
  • item two -
      -
    • nested item
    • -
    -
  • -
" -`; - -exports[`renders checkbox list 1`] = ` -"
    -
  • [ ]unchecked
  • -
  • [x]checked
  • -
" -`; - -exports[`renders code block 1`] = ` -"
this is indented code
-
" -`; - -exports[`renders code fence 1`] = ` -"
this is code
-
" -`; - -exports[`renders code marks 1`] = `"

this is inline code text

"`; - -exports[`renders headings 1`] = ` -"

Heading 1

-

Heading 2

-

Heading 3

-

Heading 4

" -`; - -exports[`renders highlight marks 1`] = `"

this is highlighted text

"`; - -exports[`renders horizontal rule 1`] = `"
"`; - -exports[`renders image 1`] = `"

\\"caption\\"

"`; - -exports[`renders image with alignment 1`] = `"

\\"caption\\"

"`; - -exports[`renders info notice 1`] = ` -"
-

content of notice

-
" -`; - -exports[`renders italic marks 1`] = `"

this is italic text

"`; - -exports[`renders italic marks 2`] = `"

this is also italic text

"`; - -exports[`renders link marks 1`] = `"

this is linked text

"`; - -exports[`renders ordered list 1`] = ` -"
    -
  1. item one
  2. -
  3. item two
  4. -
" -`; - -exports[`renders ordered list 2`] = ` -"
    -
  1. item one
  2. -
  3. item two
  4. -
" -`; - -exports[`renders plain text as paragraph 1`] = `"

plain text

"`; - -exports[`renders table 1`] = ` -" - - - - - - - - - - - - - - - -
-

heading

-

centered

-

right aligned

-

-

center

-

-

-

-

bottom r

" -`; - -exports[`renders template placeholder marks 1`] = `"

this is a placeholder

"`; - -exports[`renders tip notice 1`] = ` -"
-

content of notice

-
" -`; - -exports[`renders underline marks 1`] = `"

this is underlined text

"`; - -exports[`renders underline marks 2`] = `"

this is strikethrough text

"`; - -exports[`renders warning notice 1`] = ` -"
-

content of notice

-
" -`; diff --git a/server/editor/index.ts b/server/editor/index.ts index d4605d84d0..81f0a4026b 100644 --- a/server/editor/index.ts +++ b/server/editor/index.ts @@ -1,7 +1,6 @@ import { Schema } from "prosemirror-model"; import ExtensionManager from "@shared/editor/lib/ExtensionManager"; import fullPackage from "@shared/editor/packages/full"; -import render from "./renderToHtml"; const extensions = new ExtensionManager(fullPackage); @@ -16,6 +15,3 @@ export const parser = extensions.parser({ }); export const serializer = extensions.serializer(); - -export const renderToHtml = (markdown: string): string => - render(markdown, extensions.rulePlugins); diff --git a/server/editor/renderToHtml.test.ts b/server/editor/renderToHtml.test.ts deleted file mode 100644 index 40b1868c77..0000000000 --- a/server/editor/renderToHtml.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -import renderToHtml from "./renderToHtml"; - -test("renders an empty string", () => { - expect(renderToHtml("")).toBe(""); -}); - -test("renders plain text as paragraph", () => { - expect(renderToHtml("plain text")).toMatchSnapshot(); -}); - -test("renders blockquote", () => { - expect(renderToHtml("> blockquote")).toMatchSnapshot(); -}); - -test("renders code block", () => { - expect( - renderToHtml(` - this is indented code -`) - ).toMatchSnapshot(); -}); - -test("renders code fence", () => { - expect( - renderToHtml(`\`\`\`javascript -this is code -\`\`\``) - ).toMatchSnapshot(); -}); - -test("renders checkbox list", () => { - expect( - renderToHtml(`- [ ] unchecked -- [x] checked`) - ).toMatchSnapshot(); -}); - -test("renders bullet list", () => { - expect( - renderToHtml(`- item one -- item two - - nested item`) - ).toMatchSnapshot(); -}); - -test("renders info notice", () => { - expect( - renderToHtml(`:::info -content of notice -:::`) - ).toMatchSnapshot(); -}); - -test("renders warning notice", () => { - expect( - renderToHtml(`:::warning -content of notice -:::`) - ).toMatchSnapshot(); -}); - -test("renders tip notice", () => { - expect( - renderToHtml(`:::tip -content of notice -:::`) - ).toMatchSnapshot(); -}); - -test("renders headings", () => { - expect( - renderToHtml(`# Heading 1 - -## Heading 2 - -### Heading 3 - -#### Heading 4`) - ).toMatchSnapshot(); -}); - -test("renders horizontal rule", () => { - expect(renderToHtml(`---`)).toMatchSnapshot(); -}); - -test("renders image", () => { - expect( - renderToHtml(`![caption](https://lorempixel.com/200/200)`) - ).toMatchSnapshot(); -}); - -test("renders image with alignment", () => { - expect( - renderToHtml(`![caption](https://lorempixel.com/200/200 "left-40")`) - ).toMatchSnapshot(); -}); - -test("renders table", () => { - expect( - renderToHtml(` -| heading | centered | right aligned | -|---------|:--------:|--------------:| -| | center | | -| | | bottom r | -`) - ).toMatchSnapshot(); -}); - -test("renders bold marks", () => { - expect(renderToHtml(`this is **bold** text`)).toMatchSnapshot(); -}); - -test("renders code marks", () => { - expect(renderToHtml(`this is \`inline code\` text`)).toMatchSnapshot(); -}); - -test("renders highlight marks", () => { - expect(renderToHtml(`this is ==highlighted== text`)).toMatchSnapshot(); -}); - -test("renders italic marks", () => { - expect(renderToHtml(`this is *italic* text`)).toMatchSnapshot(); - expect(renderToHtml(`this is _also italic_ text`)).toMatchSnapshot(); -}); - -test("renders template placeholder marks", () => { - expect(renderToHtml(`this is !!a placeholder!!`)).toMatchSnapshot(); -}); - -test("renders underline marks", () => { - expect(renderToHtml(`this is __underlined__ text`)).toMatchSnapshot(); -}); - -test("renders link marks", () => { - expect( - renderToHtml(`this is [linked](https://www.example.com) text`) - ).toMatchSnapshot(); -}); - -test("renders underline marks", () => { - expect(renderToHtml(`this is ~~strikethrough~~ text`)).toMatchSnapshot(); -}); - -test("renders ordered list", () => { - expect( - renderToHtml(`1. item one -1. item two`) - ).toMatchSnapshot(); - - expect( - renderToHtml(`1. item one -2. item two`) - ).toMatchSnapshot(); -}); diff --git a/server/editor/renderToHtml.ts b/server/editor/renderToHtml.ts deleted file mode 100644 index e151ca7f32..0000000000 --- a/server/editor/renderToHtml.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { PluginSimple } from "markdown-it"; -import createMarkdown from "@shared/editor/lib/markdown/rules"; -import attachmentsRule from "@shared/editor/rules/attachments"; -import breakRule from "@shared/editor/rules/breaks"; -import checkboxRule from "@shared/editor/rules/checkboxes"; -import embedsRule from "@shared/editor/rules/embeds"; -import emojiRule from "@shared/editor/rules/emoji"; -import markRule from "@shared/editor/rules/mark"; -import noticesRule from "@shared/editor/rules/notices"; -import tablesRule from "@shared/editor/rules/tables"; -import underlinesRule from "@shared/editor/rules/underlines"; - -const defaultRules = [ - embedsRule([]), - breakRule, - checkboxRule, - markRule({ delim: "==", mark: "highlight" }), - markRule({ delim: "!!", mark: "placeholder" }), - underlinesRule, - tablesRule, - noticesRule, - attachmentsRule, - emojiRule, -]; - -export default function renderToHtml( - markdown: string, - rulePlugins: PluginSimple[] = defaultRules -): string { - return createMarkdown({ plugins: rulePlugins }).render(markdown).trim(); -} diff --git a/server/models/Document.ts b/server/models/Document.ts index 2760803c7f..26b40fa075 100644 --- a/server/models/Document.ts +++ b/server/models/Document.ts @@ -1,4 +1,3 @@ -import { updateYFragment } from "@getoutline/y-prosemirror"; import removeMarkdown from "@tommoor/remove-markdown"; import invariant from "invariant"; import { compact, find, map, uniq } from "lodash"; @@ -34,14 +33,12 @@ import { } from "sequelize-typescript"; import MarkdownSerializer from "slate-md-serializer"; import isUUID from "validator/lib/isUUID"; -import * as Y from "yjs"; import { DateFilter } from "@shared/types"; import getTasks from "@shared/utils/getTasks"; import parseTitle from "@shared/utils/parseTitle"; import unescape from "@shared/utils/unescape"; import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers"; import { DocumentValidation } from "@shared/validations"; -import { parser } from "@server/editor"; import slugify from "@server/utils/slugify"; import Backlink from "./Backlink"; import Collection from "./Collection"; @@ -482,7 +479,7 @@ class Document extends ParanoidModel { query: string, options: SearchOptions = {} ): Promise { - const wildcardQuery = `${escape(query)}:*`; + const wildcardQuery = `${escapeQuery(query)}:*`; const { snippetMinWords = 20, snippetMaxWords = 30, @@ -610,7 +607,7 @@ class Document extends ParanoidModel { limit = 15, offset = 0, } = options; - const wildcardQuery = `${escape(query)}:*`; + const wildcardQuery = `${escapeQuery(query)}:*`; // Ensure we're filtering by the users accessible collections. If // collectionId is passed as an option it is assumed that the authorization @@ -731,38 +728,6 @@ class Document extends ParanoidModel { // instance methods - updateFromMarkdown = (text: string, append = false) => { - this.text = append ? this.text + text : text; - - if (this.state) { - const ydoc = new Y.Doc(); - Y.applyUpdate(ydoc, this.state); - const type = ydoc.get("default", Y.XmlFragment) as Y.XmlFragment; - const doc = parser.parse(this.text); - - if (!type.doc) { - throw new Error("type.doc not found"); - } - - // apply new document to existing ydoc - updateYFragment(type.doc, type, doc, new Map()); - - const state = Y.encodeStateAsUpdate(ydoc); - this.state = Buffer.from(state); - this.changed("state", true); - } - }; - - toMarkdown = () => { - const text = unescape(this.text); - - if (this.version) { - return `# ${this.title}\n\n${text}`; - } - - return text; - }; - migrateVersion = () => { let migrated = false; @@ -1054,7 +1019,7 @@ class Document extends ParanoidModel { }; } -function escape(query: string): string { +function escapeQuery(query: string): string { // replace "\" with escaped "\\" because sequelize.escape doesn't do it // https://github.com/sequelize/sequelize/issues/2950 return Document.sequelize!.escape(query).replace(/\\/g, "\\\\"); diff --git a/server/models/helpers/DocumentHelper.tsx b/server/models/helpers/DocumentHelper.tsx new file mode 100644 index 0000000000..e26175ce80 --- /dev/null +++ b/server/models/helpers/DocumentHelper.tsx @@ -0,0 +1,154 @@ +import { + updateYFragment, + yDocToProsemirrorJSON, +} from "@getoutline/y-prosemirror"; +import { JSDOM } from "jsdom"; +import { Node, DOMSerializer } from "prosemirror-model"; +import * as React from "react"; +import { renderToString } from "react-dom/server"; +import styled, { ServerStyleSheet, ThemeProvider } from "styled-components"; +import * as Y from "yjs"; +import EditorContainer from "@shared/editor/components/Styles"; +import GlobalStyles from "@shared/styles/globals"; +import light from "@shared/styles/theme"; +import unescape from "@shared/utils/unescape"; +import { parser, schema } from "@server/editor"; +import Logger from "@server/logging/Logger"; +import type Document from "@server/models/Document"; + +export default class DocumentHelper { + /** + * Returns the document as a Prosemirror Node. This method uses the + * collaborative state if available, otherwise it falls back to Markdown->HTML. + * + * @param document The document to convert + * @returns The document content as a Prosemirror Node + */ + static toProsemirror(document: Document) { + if (document.state) { + const ydoc = new Y.Doc(); + Y.applyUpdate(ydoc, document.state); + return Node.fromJSON(schema, yDocToProsemirrorJSON(ydoc, "default")); + } + return parser.parse(document.text); + } + + /** + * Returns the document as Markdown. This is a lossy conversion and should + * only be used for export. + * + * @param document The document to convert + * @returns The document title and content as a Markdown string + */ + static toMarkdown(document: Document) { + const text = unescape(document.text); + + if (document.version) { + return `# ${document.title}\n\n${text}`; + } + + return text; + } + + /** + * Returns the document as plain HTML. This is a lossy conversion and should + * only be used for export. + * + * @param document The document to convert + * @returns The document title and content as a HTML string + */ + static toHTML(document: Document) { + const node = DocumentHelper.toProsemirror(document); + const sheet = new ServerStyleSheet(); + let html, styleTags; + + const Centered = styled.article` + max-width: 46em; + margin: 0 auto; + padding: 0 1em; + `; + + // First render the containing document which has all the editor styles, + // global styles, layout and title. + try { + html = renderToString( + sheet.collectStyles( + + <> + + +

{document.title}

+ +
+
+
+ +
+ ) + ); + styleTags = sheet.getStyleTags(); + } catch (error) { + Logger.error("Failed to render styles on document export", error, { + id: document.id, + }); + } finally { + sheet.seal(); + } + + // Render the Prosemirror document using virtual DOM and serialize the + // result to a string + const dom = new JSDOM(`${styleTags}${html}`); + const doc = dom.window.document; + const target = doc.getElementById("content"); + + DOMSerializer.fromSchema(schema).serializeFragment( + node.content, + { + document: doc, + }, + // @ts-expect-error incorrect library type, third argument is target node + target + ); + + return dom.serialize(); + } + + /** + * Applies the given Markdown to the document, this essentially creates a + * single change in the collaborative state that makes all the edits to get + * to the provided Markdown. + * + * @param document The document to apply the changes to + * @param text The markdown to apply + * @param append If true appends the markdown instead of replacing existing + * content + * @returns The document + */ + static applyMarkdownToDocument( + document: Document, + text: string, + append = false + ) { + document.text = append ? document.text + text : text; + + if (document.state) { + const ydoc = new Y.Doc(); + Y.applyUpdate(ydoc, document.state); + const type = ydoc.get("default", Y.XmlFragment) as Y.XmlFragment; + const doc = parser.parse(document.text); + + if (!type.doc) { + throw new Error("type.doc not found"); + } + + // apply new document to existing ydoc + updateYFragment(type.doc, type, doc, new Map()); + + const state = Y.encodeStateAsUpdate(ydoc); + document.state = Buffer.from(state); + document.changed("state", true); + } + + return document; + } +} diff --git a/server/routes/api/documents.test.ts b/server/routes/api/documents.test.ts index 35753e9651..ef477e14dd 100644 --- a/server/routes/api/documents.test.ts +++ b/server/routes/api/documents.test.ts @@ -8,6 +8,7 @@ import { SearchQuery, Event, } from "@server/models"; +import DocumentHelper from "@server/models/helpers/DocumentHelper"; import { buildShare, buildCollection, @@ -462,7 +463,22 @@ describe("#documents.export", () => { }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data).toEqual(document.toMarkdown()); + expect(body.data).toEqual(DocumentHelper.toMarkdown(document)); + }); + + it("should return document text with accept=text/markdown", async () => { + const { user, document } = await seed(); + const res = await server.post("/api/documents.export", { + body: { + token: user.getJwtToken(), + id: document.id, + }, + headers: { + accept: "text/markdown", + }, + }); + const body = await res.text(); + expect(body).toEqual(DocumentHelper.toMarkdown(document)); }); it("should return archived document", async () => { @@ -476,7 +492,7 @@ describe("#documents.export", () => { }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data).toEqual(document.toMarkdown()); + expect(body.data).toEqual(DocumentHelper.toMarkdown(document)); }); it("should not return published document in collection not a member of", async () => { @@ -509,7 +525,7 @@ describe("#documents.export", () => { }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data).toEqual(document.toMarkdown()); + expect(body.data).toEqual(DocumentHelper.toMarkdown(document)); }); it("should return document from shareId without token", async () => { @@ -526,7 +542,7 @@ describe("#documents.export", () => { }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data).toEqual(document.toMarkdown()); + expect(body.data).toEqual(DocumentHelper.toMarkdown(document)); }); it("should not return document from revoked shareId", async () => { @@ -576,7 +592,7 @@ describe("#documents.export", () => { }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data).toEqual(document.toMarkdown()); + expect(body.data).toEqual(DocumentHelper.toMarkdown(document)); }); it("should return draft document from shareId with token", async () => { @@ -596,7 +612,7 @@ describe("#documents.export", () => { }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data).toEqual(document.toMarkdown()); + expect(body.data).toEqual(DocumentHelper.toMarkdown(document)); }); it("should return document from shareId in collection not a member of", async () => { @@ -616,7 +632,7 @@ describe("#documents.export", () => { }); const body = await res.json(); expect(res.status).toEqual(200); - expect(body.data).toEqual(document.toMarkdown()); + expect(body.data).toEqual(DocumentHelper.toMarkdown(document)); }); it("should require authorization without token", async () => { diff --git a/server/routes/api/documents.ts b/server/routes/api/documents.ts index 96a35222de..4c73c86cdb 100644 --- a/server/routes/api/documents.ts +++ b/server/routes/api/documents.ts @@ -1,6 +1,7 @@ import fs from "fs-extra"; import invariant from "invariant"; import Router from "koa-router"; +import mime from "mime-types"; import { Op, ScopeOptions, WhereOptions } from "sequelize"; import { subtractDate } from "@shared/utils/date"; import documentCreator from "@server/commands/documentCreator"; @@ -27,6 +28,7 @@ import { User, View, } from "@server/models"; +import DocumentHelper from "@server/models/helpers/DocumentHelper"; import { authorize, cannot } from "@server/policies"; import { presentCollection, @@ -439,14 +441,46 @@ router.post( async (ctx) => { const { id, shareId } = ctx.body; assertPresent(id || shareId, "id or shareId is required"); + const { user } = ctx.state; + const accept = ctx.request.headers["accept"]; + const { document } = await documentLoader({ id, shareId, user, + // We need the collaborative state to generate HTML. + includeState: accept === "text/html", }); + + let contentType; + let content; + + if (accept?.includes("text/html")) { + contentType = "text/html"; + content = DocumentHelper.toHTML(document); + } else if (accept?.includes("text/markdown")) { + contentType = "text/markdown"; + content = DocumentHelper.toMarkdown(document); + } else { + contentType = "application/json"; + content = DocumentHelper.toMarkdown(document); + } + + if (contentType !== "application/json") { + ctx.set("Content-Type", contentType); + ctx.set( + "Content-Disposition", + `attachment; filename="${document.title}.${mime.extension( + contentType + )}"` + ); + ctx.body = content; + return; + } + ctx.body = { - data: document.toMarkdown(), + data: content, }; } ); diff --git a/server/utils/zip.ts b/server/utils/zip.ts index 67ba2c49ee..ca5193a4e8 100644 --- a/server/utils/zip.ts +++ b/server/utils/zip.ts @@ -8,6 +8,7 @@ import Logger from "@server/logging/Logger"; import Attachment from "@server/models/Attachment"; import Collection from "@server/models/Collection"; import Document from "@server/models/Document"; +import DocumentHelper from "@server/models/helpers/DocumentHelper"; import { NavigationNode } from "~/types"; import { deserializeFilename, serializeFilename } from "./fs"; import parseAttachmentIds from "./parseAttachmentIds"; @@ -36,7 +37,7 @@ async function addDocumentTreeToArchive( continue; } - let text = document.toMarkdown(); + let text = DocumentHelper.toMarkdown(document); const attachments = await Attachment.findAll({ where: { teamId: document.teamId, diff --git a/server/validation.ts b/server/validation.ts index 10d5fadc38..57f4f23580 100644 --- a/server/validation.ts +++ b/server/validation.ts @@ -25,7 +25,7 @@ export function assertArray( export const assertIn = ( value: string, - options: (string | undefined | null)[], + options: Primitive[], message?: string ) => { if (!options.includes(value)) { diff --git a/shared/editor/components/Styles.ts b/shared/editor/components/Styles.ts new file mode 100644 index 0000000000..83d618b766 --- /dev/null +++ b/shared/editor/components/Styles.ts @@ -0,0 +1,8 @@ +import styled from "styled-components"; +import style, { Props } from "../../styles/editor"; + +const EditorContainer = styled.div` + ${style}; +`; + +export default EditorContainer; diff --git a/shared/editor/nodes/CheckboxItem.ts b/shared/editor/nodes/CheckboxItem.ts index f7c03ff154..56b6cfaa07 100644 --- a/shared/editor/nodes/CheckboxItem.ts +++ b/shared/editor/nodes/CheckboxItem.ts @@ -33,12 +33,16 @@ export default class CheckboxItem extends Node { }, ], toDOM: (node) => { - const input = document.createElement("span"); - input.tabIndex = -1; - input.className = "checkbox"; - input.setAttribute("aria-checked", node.attrs.checked.toString()); - input.setAttribute("role", "checkbox"); - input.addEventListener("click", this.handleClick); + const checked = node.attrs.checked.toString(); + let input; + if (typeof document !== "undefined") { + input = document.createElement("span"); + input.tabIndex = -1; + input.className = "checkbox"; + input.setAttribute("aria-checked", checked); + input.setAttribute("role", "checkbox"); + input.addEventListener("click", this.handleClick); + } return [ "li", @@ -51,7 +55,9 @@ export default class CheckboxItem extends Node { { contentEditable: "false", }, - input, + ...(input + ? [input] + : [["span", { class: "checkbox", "aria-checked": checked }]]), ], ["div", 0], ]; diff --git a/shared/editor/nodes/CodeFence.ts b/shared/editor/nodes/CodeFence.ts index a82ea4e3b2..5d4913ace1 100644 --- a/shared/editor/nodes/CodeFence.ts +++ b/shared/editor/nodes/CodeFence.ts @@ -122,44 +122,53 @@ export default class CodeFence extends Node { }, ], toDOM: (node) => { - const button = document.createElement("button"); - button.innerText = this.options.dictionary.copy; - button.type = "button"; - button.addEventListener("click", this.handleCopyToClipboard); + let actions; + if (typeof document !== "undefined") { + const button = document.createElement("button"); + button.innerText = this.options.dictionary.copy; + button.type = "button"; + button.addEventListener("click", this.handleCopyToClipboard); - const select = document.createElement("select"); - select.addEventListener("change", this.handleLanguageChange); + const select = document.createElement("select"); + select.addEventListener("change", this.handleLanguageChange); - const actions = document.createElement("div"); - actions.className = "code-actions"; - actions.appendChild(select); - actions.appendChild(button); + actions = document.createElement("div"); + actions.className = "code-actions"; + actions.appendChild(select); + actions.appendChild(button); - this.languageOptions.forEach(([key, label]) => { - const option = document.createElement("option"); - const value = key === "none" ? "" : key; - option.value = value; - option.innerText = label; - option.selected = node.attrs.language === value; - select.appendChild(option); - }); + this.languageOptions.forEach(([key, label]) => { + const option = document.createElement("option"); + const value = key === "none" ? "" : key; + option.value = value; + option.innerText = label; + option.selected = node.attrs.language === value; + select.appendChild(option); + }); - // For the Mermaid language we add an extra button to toggle between - // source code and a rendered diagram view. - if (node.attrs.language === "mermaidjs") { - const showSourceButton = document.createElement("button"); - showSourceButton.innerText = this.options.dictionary.showSource; - showSourceButton.type = "button"; - showSourceButton.classList.add("show-source-button"); - showSourceButton.addEventListener("click", this.handleToggleDiagram); - actions.prepend(showSourceButton); + // For the Mermaid language we add an extra button to toggle between + // source code and a rendered diagram view. + if (node.attrs.language === "mermaidjs") { + const showSourceButton = document.createElement("button"); + showSourceButton.innerText = this.options.dictionary.showSource; + showSourceButton.type = "button"; + showSourceButton.classList.add("show-source-button"); + showSourceButton.addEventListener( + "click", + this.handleToggleDiagram + ); + actions.prepend(showSourceButton); - const showDiagramButton = document.createElement("button"); - showDiagramButton.innerText = this.options.dictionary.showDiagram; - showDiagramButton.type = "button"; - showDiagramButton.classList.add("show-digram-button"); - showDiagramButton.addEventListener("click", this.handleToggleDiagram); - actions.prepend(showDiagramButton); + const showDiagramButton = document.createElement("button"); + showDiagramButton.innerText = this.options.dictionary.showDiagram; + showDiagramButton.type = "button"; + showDiagramButton.classList.add("show-digram-button"); + showDiagramButton.addEventListener( + "click", + this.handleToggleDiagram + ); + actions.prepend(showDiagramButton); + } } return [ @@ -168,7 +177,7 @@ export default class CodeFence extends Node { class: "code-block", "data-language": node.attrs.language, }, - ["div", { contentEditable: "false" }, actions], + ...(actions ? [["div", { contentEditable: "false" }, actions]] : []), ["pre", ["code", { spellCheck: "false" }, 0]], ]; }, diff --git a/shared/editor/nodes/Heading.ts b/shared/editor/nodes/Heading.ts index e0e4af8fc8..e7f92cd303 100644 --- a/shared/editor/nodes/Heading.ts +++ b/shared/editor/nodes/Heading.ts @@ -50,23 +50,28 @@ export default class Heading extends Node { contentElement: ".heading-content", })), toDOM: (node) => { - const anchor = document.createElement("button"); - anchor.innerText = "#"; - anchor.type = "button"; - anchor.className = "heading-anchor"; - anchor.addEventListener("click", (event) => this.handleCopyLink(event)); + let anchor, fold; + if (typeof document !== "undefined") { + anchor = document.createElement("button"); + anchor.innerText = "#"; + anchor.type = "button"; + anchor.className = "heading-anchor"; + anchor.addEventListener("click", (event) => + this.handleCopyLink(event) + ); - const fold = document.createElement("button"); - fold.innerText = ""; - fold.innerHTML = - ''; - fold.type = "button"; - fold.className = `heading-fold ${ - node.attrs.collapsed ? "collapsed" : "" - }`; - fold.addEventListener("mousedown", (event) => - this.handleFoldContent(event) - ); + fold = document.createElement("button"); + fold.innerText = ""; + fold.innerHTML = + ''; + fold.type = "button"; + fold.className = `heading-fold ${ + node.attrs.collapsed ? "collapsed" : "" + }`; + fold.addEventListener("mousedown", (event) => + this.handleFoldContent(event) + ); + } return [ `h${node.attrs.level + (this.options.offset || 0)}`, @@ -78,8 +83,7 @@ export default class Heading extends Node { node.attrs.collapsed ? "collapsed" : "" }`, }, - anchor, - fold, + ...(anchor ? [anchor, fold] : []), ], [ "span", diff --git a/shared/editor/nodes/Notice.tsx b/shared/editor/nodes/Notice.tsx index 64af39d39e..158a0dfb90 100644 --- a/shared/editor/nodes/Notice.tsx +++ b/shared/editor/nodes/Notice.tsx @@ -52,40 +52,43 @@ export default class Notice extends Node { }, ], toDOM: (node) => { - const select = document.createElement("select"); - select.addEventListener("change", this.handleStyleChange); + let icon, actions; + if (typeof document !== "undefined") { + const select = document.createElement("select"); + select.addEventListener("change", this.handleStyleChange); - this.styleOptions.forEach(([key, label]) => { - const option = document.createElement("option"); - option.value = key; - option.innerText = label; - option.selected = node.attrs.style === key; - select.appendChild(option); - }); + this.styleOptions.forEach(([key, label]) => { + const option = document.createElement("option"); + option.value = key; + option.innerText = label; + option.selected = node.attrs.style === key; + select.appendChild(option); + }); - const actions = document.createElement("div"); - actions.className = "notice-actions"; - actions.appendChild(select); + actions = document.createElement("div"); + actions.className = "notice-actions"; + actions.appendChild(select); - let component; + let component; - if (node.attrs.style === "tip") { - component = ; - } else if (node.attrs.style === "warning") { - component = ; - } else { - component = ; + if (node.attrs.style === "tip") { + component = ; + } else if (node.attrs.style === "warning") { + component = ; + } else { + component = ; + } + + icon = document.createElement("div"); + icon.className = "icon"; + ReactDOM.render(component, icon); } - const icon = document.createElement("div"); - icon.className = "icon"; - ReactDOM.render(component, icon); - return [ "div", { class: `notice-block ${node.attrs.style}` }, - icon, - ["div", { contentEditable: "false" }, actions], + ...(icon ? [icon] : []), + ["div", { contentEditable: "false" }, ...(actions ? [actions] : [])], ["div", { class: "content" }, 0], ]; }, diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 6bb792195e..e0437b7afe 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -14,6 +14,8 @@ "Subscribed to document notifications": "Subscribed to document notifications", "Unsubscribe": "Unsubscribe", "Unsubscribed from document notifications": "Unsubscribed from document notifications", + "HTML": "HTML", + "Markdown": "Markdown", "Download": "Download", "Download document": "Download document", "Duplicate": "Duplicate", diff --git a/shared/styles/editor.ts b/shared/styles/editor.ts new file mode 100644 index 0000000000..b302d3710b --- /dev/null +++ b/shared/styles/editor.ts @@ -0,0 +1,1304 @@ +/* eslint-disable no-irregular-whitespace */ +import { darken, lighten, transparentize } from "polished"; +import { DefaultTheme } from "styled-components"; +import depths from "./depths"; + +export type Props = { + rtl: boolean; + readOnly?: boolean; + readOnlyWriteCheckboxes?: boolean; + grow?: boolean; + theme: DefaultTheme; +}; + +const style = (props: Props) => ` +flex-grow: ${props.grow ? 1 : 0}; +justify-content: start; +color: ${props.theme.text}; +font-family: ${props.theme.fontFamily}; +font-weight: ${props.theme.fontWeight}; +font-size: 1em; +line-height: 1.6em; +width: 100%; + +> div { + background: transparent; +} + +& * { + box-sizing: content-box; +} + +.ProseMirror { + position: relative; + outline: none; + word-wrap: break-word; + white-space: pre-wrap; + white-space: break-spaces; + -webkit-font-variant-ligatures: none; + font-variant-ligatures: none; + font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */ + + & > .ProseMirror-yjs-cursor { + display: none; + } + + & > * { + margin-top: .5em; + margin-bottom: .5em; + } + + & > :first-child, + & > button:first-child + * { + margin-top: 0; + } + + h2, + h3, + h4, + h5, + h6 { + margin-top: 1em; + } + + h1 { + margin-top: .75em; + margin-bottom: 0.25em; + } + + // all of heading sizes are stepped down one from global styles, except h1 + // which is between h1 and h2 + h1 { font-size: 1.75em; } + h2 { font-size: 1.25em; } + h3 { font-size: 1em; } + h4 { font-size: 0.875em; } + h5 { font-size: 0.75em; } + h6 { font-size: 0.75em; } + + .ProseMirror-yjs-cursor { + position: relative; + margin-left: -1px; + margin-right: -1px; + border-left: 1px solid black; + border-right: 1px solid black; + height: 1em; + word-break: normal; + + &:after { + content: ""; + display: block; + position: absolute; + left: -8px; + right: -8px; + top: 0; + bottom: 0; + } + > div { + opacity: 0; + transition: opacity 100ms ease-in-out; + position: absolute; + top: -1.8em; + font-size: 13px; + background-color: rgb(250, 129, 0); + font-style: normal; + line-height: normal; + user-select: none; + white-space: nowrap; + color: white; + padding: 2px 6px; + font-weight: 500; + border-radius: 4px; + pointer-events: none; + left: -1px; + } + + &:hover { + > div { + opacity: 1; + } + } + } +} + +&.show-cursor-names .ProseMirror-yjs-cursor > div { + opacity: 1; +} + +pre { + white-space: pre-wrap; +} + +li { + position: relative; +} + +.image { + line-height: 0; + text-align: center; + max-width: 100%; + clear: both; + + img { + pointer-events: ${props.readOnly ? "initial" : "none"}; + display: inline-block; + max-width: 100%; + max-height: 75vh; + } + + .ProseMirror-selectednode img { + pointer-events: initial; + } +} + +.image.placeholder { + position: relative; + background: ${props.theme.background}; + margin-bottom: calc(28px + 1.2em); + + img { + opacity: 0.5; + } +} + +.image-replacement-uploading { + img { + opacity: 0.5; + } +} + +.image-right-50 { + float: right; + width: 50%; + margin-left: 2em; + margin-bottom: 1em; + clear: initial; +} + +.image-left-50 { + float: left; + width: 50%; + margin-right: 2em; + margin-bottom: 1em; + clear: initial; +} + +.ProseMirror-hideselection *::selection { + background: transparent; +} +.ProseMirror-hideselection *::-moz-selection { + background: transparent; +} +.ProseMirror-hideselection { + caret-color: transparent; +} + +.ProseMirror-selectednode { + outline: 2px solid + ${props.readOnly ? "transparent" : props.theme.selected}; +} + +/* Make sure li selections wrap around markers */ + +li.ProseMirror-selectednode { + outline: none; +} + +li.ProseMirror-selectednode:after { + content: ""; + position: absolute; + left: ${props.rtl ? "-2px" : "-32px"}; + right: ${props.rtl ? "-32px" : "-2px"}; + top: -2px; + bottom: -2px; + border: 2px solid ${props.theme.selected}; + pointer-events: none; +} + +img.ProseMirror-separator { + display: inline; + border: none !important; + margin: 0 !important; +} + +// Removes forced paragraph spaces below images, this is needed to images +// being inline nodes that are displayed like blocks +.component-image + img.ProseMirror-separator, +.component-image + img.ProseMirror-separator + br.ProseMirror-trailingBreak { + display: none; +} + +.ProseMirror[contenteditable="false"] { + .caption { + pointer-events: none; + } + .caption:empty { + visibility: hidden; + } +} + +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 500; + cursor: text; + + &:not(.placeholder):before { + display: ${props.readOnly ? "none" : "inline-block"}; + font-family: ${props.theme.fontFamilyMono}; + color: ${props.theme.textSecondary}; + font-size: 13px; + line-height: 0; + margin-${props.rtl ? "right" : "left"}: -24px; + transition: opacity 150ms ease-in-out; + opacity: 0; + width: 24px; + } + + &:hover, + &:focus-within { + .heading-actions { + opacity: 1; + } + } +} + +.heading-content { + &:before { + content: "​"; + display: inline; + } +} + +.heading-name { + color: ${props.theme.text}; + pointer-events: none; + display: block; + position: relative; + top: -60px; + visibility: hidden; + + &:hover { + text-decoration: none; + } +} + +.heading-name:first-child, +.heading-name:first-child + .ProseMirror-yjs-cursor { + & + h1, + & + h2, + & + h3, + & + h4 { + margin-top: 0; + } +} + +a:first-child { + h1, + h2, + h3, + h4, + h5, + h6 { + margin-top: 0; + } +} + +h1:not(.placeholder):before { + content: "H1"; +} +h2:not(.placeholder):before { + content: "H2"; +} +h3:not(.placeholder):before { + content: "H3"; +} +h4:not(.placeholder):before { + content: "H4"; +} +h5:not(.placeholder):before { + content: "H5"; +} +h6:not(.placeholder):before { + content: "H6"; +} + +.ProseMirror-focused { + h1, + h2, + h3, + h4, + h5, + h6 { + &:not(.placeholder):before { + opacity: 1; + } + } +} + +.with-emoji { + margin-${props.rtl ? "right" : "left"}: -1em; +} + +.heading-anchor, +.heading-fold { + display: inline-block; + color: ${props.theme.text}; + opacity: .75; + cursor: pointer; + background: none; + outline: none; + border: 0; + margin: 0; + padding: 0; + text-align: left; + font-family: ${props.theme.fontFamilyMono}; + font-size: 14px; + line-height: 0; + width: 12px; + height: 24px; + + &:focus, + &:hover { + opacity: 1; + } +} + +.heading-anchor { + box-sizing: border-box; +} + +.heading-actions { + opacity: 0; + z-index: ${depths.editorHeadingActions}; + background: ${props.theme.background}; + margin-${props.rtl ? "right" : "left"}: -26px; + flex-direction: ${props.rtl ? "row-reverse" : "row"}; + display: inline-flex; + position: relative; + top: -2px; + width: 26px; + height: 24px; + + &.collapsed { + opacity: 1; + } + + &.collapsed .heading-anchor { + opacity: 0; + } + + &.collapsed .heading-fold { + opacity: 1; + } +} + +h1, +h2, +h3, +h4, +h5, +h6 { + &:hover { + .heading-anchor { + opacity: 0.75 !important; + } + .heading-anchor:hover { + opacity: 1 !important; + } + } +} + +.heading-fold { + display: inline-block; + transform-origin: center; + padding: 0; + + &.collapsed { + svg { + transform: rotate(${props.rtl ? "90deg" : "-90deg"}); + pointer-events: none; + } + transition-delay: 0.1s; + opacity: 1; + } +} + +.placeholder:before { + display: block; + opacity: 0; + transition: opacity 150ms ease-in-out; + content: ${props.readOnly ? "" : "attr(data-empty-text)"}; + pointer-events: none; + height: 0; + color: ${props.theme.placeholder}; +} + +/** Show the placeholder if focused or the first visible item nth(2) accounts for block insert trigger */ +.ProseMirror-focused .placeholder:before, +.placeholder:nth-child(1):before, +.placeholder:nth-child(2):before { + opacity: 1; +} + +.notice-block { + display: flex; + align-items: center; + background: ${transparentize(0.9, props.theme.noticeInfoBackground)}; + border-left: 4px solid ${props.theme.noticeInfoBackground}; + color: ${props.theme.noticeInfoText}; + border-radius: 4px; + padding: 8px 10px 8px 8px; + margin: 8px 0; + + a { + color: ${props.theme.noticeInfoText}; + } + + a:not(.heading-name) { + text-decoration: underline; + } + + p:first-child { + margin-top: 0; + } + + p:last-child { + margin-bottom: 0; + } +} + +.notice-block .content { + flex-grow: 1; + min-width: 0; +} + +.notice-block .icon { + width: 24px; + height: 24px; + align-self: flex-start; + margin-${props.rtl ? "left" : "right"}: 4px; + color: ${props.theme.noticeInfoBackground}; +} + +.notice-block.tip { + background: ${transparentize(0.9, props.theme.noticeTipBackground)}; + border-left: 4px solid ${props.theme.noticeTipBackground}; + color: ${props.theme.noticeTipText}; + + .icon { + color: ${props.theme.noticeTipBackground}; + } + + a { + color: ${props.theme.noticeTipText}; + } +} + +.notice-block.warning { + background: ${transparentize(0.9, props.theme.noticeWarningBackground)}; + border-left: 4px solid ${props.theme.noticeWarningBackground}; + color: ${props.theme.noticeWarningText}; + + .icon { + color: ${props.theme.noticeWarningBackground}; + } + + a { + color: ${props.theme.noticeWarningText}; + } +} + +blockquote { + margin: 0; + padding-left: 1.5em; + font-style: italic; + overflow: hidden; + position: relative; + + &:before { + content: ""; + display: inline-block; + width: 2px; + border-radius: 1px; + position: absolute; + margin-${props.rtl ? "right" : "left"}: -1.5em; + top: 0; + bottom: 0; + background: ${props.theme.quote}; + } +} + +b, +strong { + font-weight: 600; +} + +.template-placeholder { + color: ${props.theme.placeholder}; + border-bottom: 1px dotted ${props.theme.placeholder}; + border-radius: 2px; + cursor: text; + + &:hover { + border-bottom: 1px dotted + ${props.readOnly ? props.theme.placeholder : props.theme.textSecondary}; + } +} + +p { + margin: 0; + + span:first-child + br:last-child { + display: none; + } + + a { + color: ${props.theme.text}; + text-decoration: underline; + text-decoration-color: ${lighten(0.5, props.theme.text)}; + text-decoration-thickness: 1px; + text-underline-offset: .15em; + font-weight: 500; + + &:hover { + text-decoration: underline; + text-decoration-color: ${props.theme.text}; + text-decoration-thickness: 1px; + } + } +} + +a { + color: ${props.theme.link}; + cursor: pointer; +} + +.ProseMirror-focused { + a { + cursor: text; + } +} + +a:hover { + text-decoration: ${props.readOnly ? "underline" : "none"}; +} + +ul, +ol { + margin: ${props.rtl ? "0 -26px 0 0.1em" : "0 0.1em 0 -26px"}; + padding: ${props.rtl ? "0 44px 0 0" : "0 0 0 44px"}; +} + +ol ol { + list-style: lower-alpha; +} + +ol ol ol { + list-style: lower-roman; +} + +ul.checkbox_list { + list-style: none; + padding: 0; + margin-left: ${props.rtl ? "0" : "-24px"}; + margin-right: ${props.rtl ? "-24px" : "0"}; +} + +ul li, +ol li { + position: relative; + white-space: initial; + + p { + white-space: pre-wrap; + } + + > div { + width: 100%; + } +} + +ul.checkbox_list li { + display: flex; + padding-${props.rtl ? "right" : "left"}: 24px; +} + +ul.checkbox_list li.checked > div > p { + color: ${props.theme.textSecondary}; + text-decoration: line-through; +} + +ul li::before, +ol li::before { + background: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3QgeD0iOCIgeT0iNyIgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIGZpbGw9IiM0RTVDNkUiLz4KPHJlY3QgeD0iOCIgeT0iMTEiIHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiBmaWxsPSIjNEU1QzZFIi8+CjxyZWN0IHg9IjgiIHk9IjE1IiB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgZmlsbD0iIzRFNUM2RSIvPgo8cmVjdCB4PSIxMyIgeT0iNyIgd2lkdGg9IjMiIGhlaWdodD0iMiIgcng9IjEiIGZpbGw9IiM0RTVDNkUiLz4KPHJlY3QgeD0iMTMiIHk9IjExIiB3aWR0aD0iMyIgaGVpZ2h0PSIyIiByeD0iMSIgZmlsbD0iIzRFNUM2RSIvPgo8cmVjdCB4PSIxMyIgeT0iMTUiIHdpZHRoPSIzIiBoZWlnaHQ9IjIiIHJ4PSIxIiBmaWxsPSIjNEU1QzZFIi8+Cjwvc3ZnPgo=") no-repeat; + background-position: 0 2px; + content: ""; + display: ${props.readOnly ? "none" : "inline-block"}; + cursor: grab; + width: 24px; + height: 24px; + position: absolute; + ${props.rtl ? "right" : "left"}: -40px; + opacity: 0; + transition: opacity 200ms ease-in-out; +} + +ul li[draggable=true]::before, +ol li[draggable=true]::before { + cursor: grabbing; +} + +ul > li.counter-2::before, +ol li.counter-2::before { + ${props.rtl ? "right" : "left"}: -50px; +} + +ul > li.hovering::before, +ol li.hovering::before { + opacity: 0.5; +} + +ul li.ProseMirror-selectednode::after, +ol li.ProseMirror-selectednode::after { + display: none; +} + +ul.checkbox_list li::before { + ${props.rtl ? "right" : "left"}: 0; +} + +ul.checkbox_list li .checkbox { + display: inline-block; + cursor: pointer; + pointer-events: ${ + props.readOnly && !props.readOnlyWriteCheckboxes ? "none" : "initial" + }; + opacity: ${props.readOnly && !props.readOnlyWriteCheckboxes ? 0.75 : 1}; + margin: ${props.rtl ? "0 0 0 0.5em" : "0 0.5em 0 0"}; + width: 14px; + height: 14px; + position: relative; + top: 1px; + transition: transform 100ms ease-in-out; + opacity: .8; + + background-image: ${`url("data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3 0C1.34315 0 0 1.34315 0 3V11C0 12.6569 1.34315 14 3 14H11C12.6569 14 14 12.6569 14 11V3C14 1.34315 12.6569 0 11 0H3ZM3 2C2.44772 2 2 2.44772 2 3V11C2 11.5523 2.44772 12 3 12H11C11.5523 12 12 11.5523 12 11V3C12 2.44772 11.5523 2 11 2H3Z' fill='${props.theme.text.replace( + "#", + "%23" + )}' /%3E%3C/svg%3E%0A");`} + + &[aria-checked=true] { + opacity: 1; + background-image: ${`url( + "data:image/svg+xml,%3Csvg width='14' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3 0C1.34315 0 0 1.34315 0 3V11C0 12.6569 1.34315 14 3 14H11C12.6569 14 14 12.6569 14 11V3C14 1.34315 12.6569 0 11 0H3ZM4.26825 5.85982L5.95873 7.88839L9.70003 2.9C10.0314 2.45817 10.6582 2.36863 11.1 2.7C11.5419 3.03137 11.6314 3.65817 11.3 4.1L6.80002 10.1C6.41275 10.6164 5.64501 10.636 5.2318 10.1402L2.7318 7.14018C2.37824 6.71591 2.43556 6.08534 2.85984 5.73178C3.28412 5.37821 3.91468 5.43554 4.26825 5.85982Z' fill='${props.theme.primary.replace( + "#", + "%23" + )}' /%3E%3C/svg%3E%0A" + )`}; + } + + &:active { + transform: scale(0.9); + } +} + +li p:first-child { + margin: 0; + word-break: break-word; +} + +hr { + position: relative; + height: 1em; + border: 0; +} + +hr:before { + content: ""; + display: block; + position: absolute; + border-top: 1px solid ${props.theme.horizontalRule}; + top: 0.5em; + left: 0; + right: 0; +} + +hr.page-break { + page-break-after: always; +} + +hr.page-break:before { + border-top: 1px dashed ${props.theme.horizontalRule}; +} + +code { + border-radius: 4px; + border: 1px solid ${props.theme.codeBorder}; + background: ${props.theme.codeBackground}; + padding: 3px 4px; + font-family: ${props.theme.fontFamilyMono}; + font-size: 80%; +} + +mark { + border-radius: 1px; + color: ${props.theme.textHighlightForeground}; + background: ${props.theme.textHighlight}; + + a { + color: ${props.theme.textHighlightForeground}; + } +} + +.external-link { + display: inline-block; + position: relative; + top: 2px; + width: 16px; + height: 16px; +} + +.code-actions, +.notice-actions { + display: flex; + align-items: center; + gap: 8px; + position: absolute; + z-index: 1; + top: 8px; + right: 8px; +} + +.notice-actions { + ${props.rtl ? "left" : "right"}: 8px; +} + +.code-block, +.notice-block { + position: relative; + + select, + button { + margin: 0; + padding: 0; + border: 0; + background: ${props.theme.buttonNeutralBackground}; + color: ${props.theme.buttonNeutralText}; + box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${ + props.theme.buttonNeutralBorder + } 0 0 0 1px inset; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + text-decoration: none; + flex-shrink: 0; + cursor: pointer; + user-select: none; + appearance: none !important; + padding: 6px 8px; + display: none; + + &::-moz-focus-inner { + padding: 0; + border: 0; + } + + &:hover:not(:disabled) { + background-color: ${darken(0.05, props.theme.buttonNeutralBackground)}; + box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 2px, ${ + props.theme.buttonNeutralBorder + } 0 0 0 1px inset; + } + } + + select { + background-image: url('data:image/svg+xml;utf8, '); + background-repeat: no-repeat; + background-position: center right; + padding-right: 20px; + } + + &:focus-within, + &:hover { + select { + display: ${props.readOnly ? "none" : "inline"}; + } + + button { + display: inline; + } + } + + select:focus, + select:active, + button:focus, + button:active { + display: inline; + } + + button.show-source-button { + display: none; + } + button.show-diagram-button { + display: inline; + } + + &.code-hidden { + button, + select, + button.show-diagram-button { + display: none; + } + + button.show-source-button { + display: inline; + } + + pre { + display: none; + } + } +} + +.mermaid-diagram-wrapper { + display: flex; + align-items: center; + justify-content: center; + background: ${props.theme.codeBackground}; + border-radius: 6px; + border: 1px solid ${props.theme.codeBorder}; + padding: 8px; + user-select: none; + cursor: default; + + * { + font-family: ${props.theme.fontFamily}; + } + + &.diagram-hidden { + display: none; + } +} + +pre { + display: block; + overflow-x: auto; + padding: 0.75em 1em; + line-height: 1.4em; + position: relative; + background: ${props.theme.codeBackground}; + border-radius: 4px; + border: 1px solid ${props.theme.codeBorder}; + margin: .5em 0; + + -webkit-font-smoothing: initial; + font-family: ${props.theme.fontFamilyMono}; + font-size: 13px; + 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: ${props.theme.code}; + + code { + font-size: 13px; + background: none; + padding: 0; + border: 0; + } +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: ${props.theme.codeComment}; +} + +.token.punctuation { + color: ${props.theme.codePunctuation}; +} + +.token.namespace { + opacity: 0.7; +} + +.token.operator, +.token.boolean, +.token.number { + color: ${props.theme.codeNumber}; +} + +.token.property { + color: ${props.theme.codeProperty}; +} + +.token.tag { + color: ${props.theme.codeTag}; +} + +.token.string { + color: ${props.theme.codeString}; +} + +.token.selector { + color: ${props.theme.codeSelector}; +} + +.token.attr-name { + color: ${props.theme.codeAttr}; +} + +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: ${props.theme.codeEntity}; +} + +.token.attr-value, +.token.keyword, +.token.control, +.token.directive, +.token.unit { + color: ${props.theme.codeKeyword}; +} + +.token.function { + color: ${props.theme.codeFunction}; +} + +.token.statement, +.token.regex, +.token.atrule { + color: ${props.theme.codeStatement}; +} + +.token.placeholder, +.token.variable { + color: ${props.theme.codePlaceholder}; +} + +.token.deleted { + text-decoration: line-through; +} + +.token.inserted { + border-bottom: 1px dotted ${props.theme.codeInserted}; + text-decoration: none; +} + +.token.italic { + font-style: italic; +} + +.token.important, +.token.bold { + font-weight: bold; +} + +.token.important { + color: ${props.theme.codeImportant}; +} + +.token.entity { + cursor: help; +} + +table { + width: 100%; + border-collapse: collapse; + border-radius: 4px; + margin-top: 1em; + box-sizing: border-box; + + * { + box-sizing: border-box; + } + + tr { + position: relative; + border-bottom: 1px solid ${props.theme.tableDivider}; + } + + th { + background: ${props.theme.tableHeaderBackground}; + } + + td, + th { + position: relative; + vertical-align: top; + border: 1px solid ${props.theme.tableDivider}; + position: relative; + padding: 4px 8px; + text-align: ${props.rtl ? "right" : "left"}; + min-width: 100px; + } + + .selectedCell { + background: ${ + props.readOnly ? "inherit" : props.theme.tableSelectedBackground + }; + + /* fixes Firefox background color painting over border: + * https://bugzilla.mozilla.org/show_bug.cgi?id=688556 */ + background-clip: padding-box; + } + + .grip-column { + /* usage of ::after for all of the table grips works around a bug in + * prosemirror-tables that causes Safari to hang when selecting a cell + * in an empty table: + * https://github.com/ProseMirror/prosemirror/issues/947 */ + &::after { + content: ""; + cursor: pointer; + position: absolute; + top: -16px; + ${props.rtl ? "right" : "left"}: 0; + width: 100%; + height: 12px; + background: ${props.theme.tableDivider}; + border-bottom: 3px solid ${props.theme.background}; + display: ${props.readOnly ? "none" : "block"}; + } + + &:hover::after { + background: ${props.theme.text}; + } + &.first::after { + border-top-${props.rtl ? "right" : "left"}-radius: 3px; + } + &.last::after { + border-top-${props.rtl ? "left" : "right"}-radius: 3px; + } + &.selected::after { + background: ${props.theme.tableSelected}; + } + } + + .grip-row { + &::after { + content: ""; + cursor: pointer; + position: absolute; + ${props.rtl ? "right" : "left"}: -16px; + top: 0; + height: 100%; + width: 12px; + background: ${props.theme.tableDivider}; + border-${props.rtl ? "left" : "right"}: 3px solid; + border-color: ${props.theme.background}; + display: ${props.readOnly ? "none" : "block"}; + } + + &:hover::after { + background: ${props.theme.text}; + } + &.first::after { + border-top-${props.rtl ? "right" : "left"}-radius: 3px; + } + &.last::after { + border-bottom-${props.rtl ? "right" : "left"}-radius: 3px; + } + &.selected::after { + background: ${props.theme.tableSelected}; + } + } + + .grip-table { + &::after { + content: ""; + cursor: pointer; + background: ${props.theme.tableDivider}; + width: 13px; + height: 13px; + border-radius: 13px; + border: 2px solid ${props.theme.background}; + position: absolute; + top: -18px; + ${props.rtl ? "right" : "left"}: -18px; + display: ${props.readOnly ? "none" : "block"}; + } + + &:hover::after { + background: ${props.theme.text}; + } + &.selected::after { + background: ${props.theme.tableSelected}; + } + } +} + +.scrollable-wrapper { + position: relative; + margin: 0.5em 0px; + scrollbar-width: thin; + scrollbar-color: transparent transparent; + + &:hover { + scrollbar-color: ${props.theme.scrollbarThumb} ${ + props.theme.scrollbarBackground +}; + } + + & ::-webkit-scrollbar { + height: 14px; + background-color: transparent; + } + + &:hover ::-webkit-scrollbar { + background-color: ${props.theme.scrollbarBackground}; + } + + & ::-webkit-scrollbar-thumb { + background-color: transparent; + border: 3px solid transparent; + border-radius: 7px; + } + + &:hover ::-webkit-scrollbar-thumb { + background-color: ${props.theme.scrollbarThumb}; + border-color: ${props.theme.scrollbarBackground}; + } +} + +.scrollable { + overflow-y: hidden; + overflow-x: auto; + padding-${props.rtl ? "right" : "left"}: 1em; + margin-${props.rtl ? "right" : "left"}: -1em; + border-${props.rtl ? "right" : "left"}: 1px solid transparent; + border-${props.rtl ? "left" : "right"}: 1px solid transparent; + transition: border 250ms ease-in-out 0s; +} + +.scrollable-shadow { + position: absolute; + top: 0; + bottom: 0; + ${props.rtl ? "right" : "left"}: -1em; + width: 16px; + transition: box-shadow 250ms ease-in-out; + border: 0px solid transparent; + border-${props.rtl ? "right" : "left"}-width: 1em; + pointer-events: none; + + &.left { + box-shadow: 16px 0 16px -16px inset rgba(0, 0, 0, 0.25); + border-left: 1em solid ${props.theme.background}; + } + + &.right { + right: 0; + left: auto; + box-shadow: -16px 0 16px -16px inset rgba(0, 0, 0, 0.25); + } +} + +.block-menu-trigger { + opacity: 0; + pointer-events: none; + display: ${props.readOnly ? "none" : "inline"}; + width: 24px; + height: 24px; + color: ${props.theme.textSecondary}; + background: none; + position: absolute; + transition: color 150ms cubic-bezier(0.175, 0.885, 0.32, 1.275), + opacity 150ms ease-in-out; + outline: none; + border: 0; + padding: 0; + margin-top: 1px; + margin-${props.rtl ? "right" : "left"}: -28px; + border-radius: 4px; + + &:hover, + &:focus { + cursor: pointer; + color: ${props.theme.text}; + background: ${props.theme.secondaryBackground}; + } +} + +.ProseMirror-focused .block-menu-trigger, +.block-menu-trigger:active, +.block-menu-trigger:focus { + opacity: 1; + pointer-events: initial; +} + +.ProseMirror-gapcursor { + display: none; + pointer-events: none; + position: absolute; +} + +.ProseMirror-gapcursor:after { + content: ""; + display: block; + position: absolute; + top: -2px; + width: 20px; + border-top: 1px solid ${props.theme.cursor}; + animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; +} + +.folded-content { + display: none; +} + +@keyframes ProseMirror-cursor-blink { + to { + visibility: hidden; + } +} + +.ProseMirror-focused .ProseMirror-gapcursor { + display: block; +} + +@media print { + .placeholder:before, + .block-menu-trigger, + .heading-actions, + button.show-source-button, + h1:not(.placeholder):before, + h2:not(.placeholder):before, + h3:not(.placeholder):before, + h4:not(.placeholder):before, + h5:not(.placeholder):before, + h6:not(.placeholder):before { + display: none; + } + + .page-break { + opacity: 0; + } + + pre { + overflow-x: hidden; + white-space: pre-wrap; + } + + em, + blockquote { + font-family: "SF Pro Text", ${props.theme.fontFamily}; + } +} +`; + +export default style; diff --git a/app/styles/globals.ts b/shared/styles/globals.ts similarity index 97% rename from app/styles/globals.ts rename to shared/styles/globals.ts index 464ea35ffa..6761d8c43c 100644 --- a/app/styles/globals.ts +++ b/shared/styles/globals.ts @@ -1,6 +1,6 @@ import { createGlobalStyle } from "styled-components"; import styledNormalize from "styled-normalize"; -import { breakpoints, depths } from "@shared/styles"; +import { breakpoints, depths } from "."; export default createGlobalStyle` ${styledNormalize} diff --git a/yarn.lock b/yarn.lock index d82c2432d6..93879b0cc0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3680,7 +3680,7 @@ acorn@^7.1.1, acorn@^7.4.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.5.0: +acorn@^8.5.0, acorn@^8.7.1: version "8.8.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== @@ -5985,7 +5985,7 @@ damerau-levenshtein@^1.0.6: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== -data-urls@^3.0.1: +data-urls@^3.0.1, data-urls@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== @@ -6636,6 +6636,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== +entities@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" + integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== + entities@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" @@ -8357,10 +8362,10 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" debug "4" @@ -9617,6 +9622,39 @@ jsdom@^19.0.0: ws "^8.2.3" xml-name-validator "^4.0.0" +jsdom@^20.0.0: + version "20.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.0.tgz#882825ac9cc5e5bbee704ba16143e1fa78361ebf" + integrity sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA== + dependencies: + abab "^2.0.6" + acorn "^8.7.1" + acorn-globals "^6.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.2" + decimal.js "^10.3.1" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "^7.0.0" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^3.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + ws "^8.8.0" + xml-name-validator "^4.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -11560,6 +11598,13 @@ parse5@6.0.1, parse5@^6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +parse5@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.1.tgz#4649f940ccfb95d8754f37f73078ea20afe0c746" + integrity sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg== + dependencies: + entities "^4.4.0" + parseqs@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5" @@ -13224,6 +13269,13 @@ saxes@^5.0.1: dependencies: xmlchars "^2.2.0" +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + scheduler@^0.19.1: version "0.19.1" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" @@ -15638,7 +15690,7 @@ ws@^7.5.3: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== -ws@^8.2.3, ws@^8.5.0: +ws@^8.2.3, ws@^8.5.0, ws@^8.8.0: version "8.8.1" resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==