feat: Add HTML export option (#4056)

* tidy

* Add title to HTML export

* fix: Add compatability for documents without collab state

* Add HTML download option to UI

* docs

* fix nodes that required document to render

* Refactor to allow for styling of HTML export

* div>article for easier programatic content extraction
This commit is contained in:
Tom Moor
2022-09-07 13:34:39 +02:00
committed by GitHub
parent eb5126335c
commit e8a6de3f18
30 changed files with 1756 additions and 1790 deletions

View File

@@ -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: <DownloadIcon />,
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: <DownloadIcon />,
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: <DownloadIcon />,
keywords: "export",
children: [downloadDocumentAsHTML, downloadDocumentAsMarkdown],
});
export const duplicateDocument = createAction({
name: ({ t, isContextMenu }) =>
isContextMenu ? t("Duplicate") : t("Duplicate document"),

View File

@@ -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();

File diff suppressed because it is too large Load Diff

View File

@@ -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";

View File

@@ -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: <HistoryIcon />,
},
actionToMenuItem(downloadDocument, context),
{
type: "button",
title: t("Print"),

View File

@@ -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,
},
}
);
};
}

View File

@@ -1,111 +0,0 @@
import { createGlobalStyle } from "styled-components";
import styledNormalize from "styled-normalize";
import { breakpoints, depths } from "@shared/styles";
export default createGlobalStyle`
${styledNormalize}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
body,
button,
input,
optgroup,
select,
textarea {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
body {
font-size: 16px;
line-height: 1.5;
color: ${(props) => props.theme.text};
overscroll-behavior-y: none;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
@media (min-width: ${breakpoints.tablet}px) {
html,
body {
min-height: 100vh;
}
}
@media (min-width: ${breakpoints.tablet}px) and (display-mode: standalone) {
body:after {
content: "";
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 1px;
background: ${(props) => props.theme.titleBarDivider};
z-index: ${depths.titleBarDivider};
}
}
a {
color: ${(props) => props.theme.link};
text-decoration: none;
cursor: pointer;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 500;
line-height: 1.25;
margin-top: 1em;
margin-bottom: 0.5em;
}
h1 { font-size: 2.25em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.25em; }
h4 { font-size: 1em; }
h5 { font-size: 0.875em; }
h6 { font-size: 0.75em; }
p,
dl,
ol,
ul,
pre,
blockquote {
margin-top: 1em;
margin-bottom: 1em;
}
hr {
border: 0;
height: 0;
border-top: 1px solid ${(props) => props.theme.divider};
}
.js-focus-visible :focus:not(.focus-visible) {
outline: none;
}
.js-focus-visible .focus-visible {
outline-color: ${(props) => props.theme.primary};
outline-offset: -1px;
}
`;

View File

@@ -25,6 +25,7 @@ type Options = {
type FetchOptions = {
download?: boolean;
headers?: Record<string, string>;
};
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

View File

@@ -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();