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:
@@ -179,12 +179,12 @@ export const unsubscribeDocument = createAction({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const downloadDocument = createAction({
|
export const downloadDocumentAsHTML = createAction({
|
||||||
name: ({ t, isContextMenu }) =>
|
name: ({ t }) => t("HTML"),
|
||||||
isContextMenu ? t("Download") : t("Download document"),
|
|
||||||
section: DocumentSection,
|
section: DocumentSection,
|
||||||
|
keywords: "html export",
|
||||||
icon: <DownloadIcon />,
|
icon: <DownloadIcon />,
|
||||||
keywords: "export",
|
iconInContextMenu: false,
|
||||||
visible: ({ activeDocumentId, stores }) =>
|
visible: ({ activeDocumentId, stores }) =>
|
||||||
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
|
!!activeDocumentId && stores.policies.abilities(activeDocumentId).download,
|
||||||
perform: ({ activeDocumentId, stores }) => {
|
perform: ({ activeDocumentId, stores }) => {
|
||||||
@@ -193,10 +193,37 @@ export const downloadDocument = createAction({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const document = stores.documents.get(activeDocumentId);
|
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({
|
export const duplicateDocument = createAction({
|
||||||
name: ({ t, isContextMenu }) =>
|
name: ({ t, isContextMenu }) =>
|
||||||
isContextMenu ? t("Duplicate") : t("Duplicate document"),
|
isContextMenu ? t("Duplicate") : t("Duplicate document"),
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { observer } from "mobx-react";
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ThemeProvider } from "styled-components";
|
import { ThemeProvider } from "styled-components";
|
||||||
import { breakpoints } from "@shared/styles";
|
import { breakpoints } from "@shared/styles";
|
||||||
|
import GlobalStyles from "@shared/styles/globals";
|
||||||
import { dark, light, lightMobile, darkMobile } from "@shared/styles/theme";
|
import { dark, light, lightMobile, darkMobile } from "@shared/styles/theme";
|
||||||
import useMediaQuery from "~/hooks/useMediaQuery";
|
import useMediaQuery from "~/hooks/useMediaQuery";
|
||||||
import useStores from "~/hooks/useStores";
|
import useStores from "~/hooks/useStores";
|
||||||
import GlobalStyles from "~/styles/globals";
|
|
||||||
|
|
||||||
const Theme: React.FC = ({ children }) => {
|
const Theme: React.FC = ({ children }) => {
|
||||||
const { ui } = useStores();
|
const { ui } = useStores();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ import { EditorState, Selection, Plugin, Transaction } from "prosemirror-state";
|
|||||||
import { Decoration, EditorView } from "prosemirror-view";
|
import { Decoration, EditorView } from "prosemirror-view";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { DefaultTheme, ThemeProps } from "styled-components";
|
import { DefaultTheme, ThemeProps } from "styled-components";
|
||||||
|
import EditorContainer from "@shared/editor/components/Styles";
|
||||||
import { EmbedDescriptor } from "@shared/editor/embeds";
|
import { EmbedDescriptor } from "@shared/editor/embeds";
|
||||||
import Extension, { CommandFactory } from "@shared/editor/lib/Extension";
|
import Extension, { CommandFactory } from "@shared/editor/lib/Extension";
|
||||||
import ExtensionManager from "@shared/editor/lib/ExtensionManager";
|
import ExtensionManager from "@shared/editor/lib/ExtensionManager";
|
||||||
@@ -40,7 +41,6 @@ import EmojiMenu from "./components/EmojiMenu";
|
|||||||
import { SearchResult } from "./components/LinkEditor";
|
import { SearchResult } from "./components/LinkEditor";
|
||||||
import LinkToolbar from "./components/LinkToolbar";
|
import LinkToolbar from "./components/LinkToolbar";
|
||||||
import SelectionToolbar from "./components/SelectionToolbar";
|
import SelectionToolbar from "./components/SelectionToolbar";
|
||||||
import EditorContainer from "./components/Styles";
|
|
||||||
import WithTheme from "./components/WithTheme";
|
import WithTheme from "./components/WithTheme";
|
||||||
|
|
||||||
export { default as Extension } from "@shared/editor/lib/Extension";
|
export { default as Extension } from "@shared/editor/lib/Extension";
|
||||||
|
|||||||
@@ -296,6 +296,7 @@ function DocumentMenu({
|
|||||||
{
|
{
|
||||||
type: "separator",
|
type: "separator",
|
||||||
},
|
},
|
||||||
|
actionToMenuItem(downloadDocument, context),
|
||||||
{
|
{
|
||||||
type: "route",
|
type: "route",
|
||||||
title: t("History"),
|
title: t("History"),
|
||||||
@@ -305,7 +306,6 @@ function DocumentMenu({
|
|||||||
visible: canViewHistory,
|
visible: canViewHistory,
|
||||||
icon: <HistoryIcon />,
|
icon: <HistoryIcon />,
|
||||||
},
|
},
|
||||||
actionToMenuItem(downloadDocument, context),
|
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
title: t("Print"),
|
title: t("Print"),
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { addDays, differenceInDays } from "date-fns";
|
|||||||
import { floor } from "lodash";
|
import { floor } from "lodash";
|
||||||
import { action, autorun, computed, observable, set } from "mobx";
|
import { action, autorun, computed, observable, set } from "mobx";
|
||||||
import parseTitle from "@shared/utils/parseTitle";
|
import parseTitle from "@shared/utils/parseTitle";
|
||||||
import unescape from "@shared/utils/unescape";
|
|
||||||
import DocumentsStore from "~/stores/DocumentsStore";
|
import DocumentsStore from "~/stores/DocumentsStore";
|
||||||
import User from "~/models/User";
|
import User from "~/models/User";
|
||||||
import type { NavigationNode } from "~/types";
|
import type { NavigationNode } from "~/types";
|
||||||
|
import { client } from "~/utils/ApiClient";
|
||||||
import Storage from "~/utils/Storage";
|
import Storage from "~/utils/Storage";
|
||||||
import ParanoidModel from "./ParanoidModel";
|
import ParanoidModel from "./ParanoidModel";
|
||||||
import View from "./View";
|
import View from "./View";
|
||||||
@@ -419,21 +419,18 @@ export default class Document extends ParanoidModel {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
download = async () => {
|
download = async (contentType: "text/html" | "text/markdown") => {
|
||||||
// Ensure the document is upto date with latest server contents
|
await client.post(
|
||||||
await this.fetch();
|
`/documents.export`,
|
||||||
const body = unescape(this.text);
|
{
|
||||||
const blob = new Blob([`# ${this.title}\n\n${body}`], {
|
id: this.id,
|
||||||
type: "text/markdown",
|
},
|
||||||
});
|
{
|
||||||
const url = URL.createObjectURL(blob);
|
download: true,
|
||||||
const a = document.createElement("a");
|
headers: {
|
||||||
// Firefox support requires the anchor tag be in the DOM to trigger the dl
|
accept: contentType,
|
||||||
if (document.body) {
|
},
|
||||||
document.body.appendChild(a);
|
}
|
||||||
}
|
);
|
||||||
a.href = url;
|
|
||||||
a.download = `${this.titleWithDefault}.md`;
|
|
||||||
a.click();
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Options = {
|
|||||||
|
|
||||||
type FetchOptions = {
|
type FetchOptions = {
|
||||||
download?: boolean;
|
download?: boolean;
|
||||||
|
headers?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchWithRetry = retry(fetch);
|
const fetchWithRetry = retry(fetch);
|
||||||
@@ -81,6 +82,7 @@ class ApiClient {
|
|||||||
"cache-control": "no-cache",
|
"cache-control": "no-cache",
|
||||||
"x-editor-version": EDITOR_VERSION,
|
"x-editor-version": EDITOR_VERSION,
|
||||||
pragma: "no-cache",
|
pragma: "no-cache",
|
||||||
|
...options?.headers,
|
||||||
};
|
};
|
||||||
|
|
||||||
// for multipart forms or other non JSON requests fetch
|
// for multipart forms or other non JSON requests fetch
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ export default function download(
|
|||||||
if ("download" in a) {
|
if ("download" in a) {
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.setAttribute("download", fn);
|
a.setAttribute("download", fn);
|
||||||
a.innerHTML = "downloading…";
|
|
||||||
D.body && D.body.appendChild(a);
|
D.body && D.body.appendChild(a);
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
a.click();
|
a.click();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server",
|
"build": "yarn clean && yarn build:webpack && yarn build:i18n && yarn build:server",
|
||||||
"start": "node ./build/server/index.js",
|
"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": "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",
|
"lint": "eslint app server shared",
|
||||||
"deploy": "git push heroku master",
|
"deploy": "git push heroku master",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
@@ -108,6 +108,7 @@
|
|||||||
"invariant": "^2.2.4",
|
"invariant": "^2.2.4",
|
||||||
"ioredis": "^4.28.5",
|
"ioredis": "^4.28.5",
|
||||||
"is-printable-key-event": "^1.0.0",
|
"is-printable-key-event": "^1.0.0",
|
||||||
|
"jsdom": "^20.0.0",
|
||||||
"json-loader": "0.5.4",
|
"json-loader": "0.5.4",
|
||||||
"jsonwebtoken": "^8.5.0",
|
"jsonwebtoken": "^8.5.0",
|
||||||
"jszip": "^3.10.0",
|
"jszip": "^3.10.0",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Props = {
|
|||||||
id?: string;
|
id?: string;
|
||||||
shareId?: string;
|
shareId?: string;
|
||||||
user?: User;
|
user?: User;
|
||||||
|
includeState?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result = {
|
type Result = {
|
||||||
@@ -25,6 +26,7 @@ export default async function loadDocument({
|
|||||||
id,
|
id,
|
||||||
shareId,
|
shareId,
|
||||||
user,
|
user,
|
||||||
|
includeState,
|
||||||
}: Props): Promise<Result> {
|
}: Props): Promise<Result> {
|
||||||
let document;
|
let document;
|
||||||
let collection;
|
let collection;
|
||||||
@@ -156,6 +158,7 @@ export default async function loadDocument({
|
|||||||
document = await Document.findByPk(id as string, {
|
document = await Document.findByPk(id as string, {
|
||||||
userId: user ? user.id : undefined,
|
userId: user ? user.id : undefined,
|
||||||
paranoid: false,
|
paranoid: false,
|
||||||
|
includeState,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!document) {
|
if (!document) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { Event, Document, User } from "@server/models";
|
import { Event, Document, User } from "@server/models";
|
||||||
|
import DocumentHelper from "@server/models/helpers/DocumentHelper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
/** The user updating the document */
|
/** The user updating the document */
|
||||||
@@ -62,7 +63,7 @@ export default async function documentUpdater({
|
|||||||
}
|
}
|
||||||
if (text !== undefined) {
|
if (text !== undefined) {
|
||||||
if (user.team?.collaborativeEditing) {
|
if (user.team?.collaborativeEditing) {
|
||||||
document.updateFromMarkdown(text, append);
|
document = DocumentHelper.applyMarkdownToDocument(document, text, append);
|
||||||
} else if (append) {
|
} else if (append) {
|
||||||
document.text += text;
|
document.text += text;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`renders blockquote 1`] = `
|
|
||||||
"<blockquote>
|
|
||||||
<p>blockquote</p>
|
|
||||||
</blockquote>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders bold marks 1`] = `"<p>this is <strong>bold</strong> text</p>"`;
|
|
||||||
|
|
||||||
exports[`renders bullet list 1`] = `
|
|
||||||
"<ul>
|
|
||||||
<li>item one</li>
|
|
||||||
<li>item two
|
|
||||||
<ul>
|
|
||||||
<li>nested item</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders checkbox list 1`] = `
|
|
||||||
"<ul>
|
|
||||||
<li class=\\"checkbox-list-item\\"><span class=\\"checkbox \\">[ ]</span>unchecked</li>
|
|
||||||
<li class=\\"checkbox-list-item\\"><span class=\\"checkbox checked\\">[x]</span>checked</li>
|
|
||||||
</ul>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders code block 1`] = `
|
|
||||||
"<pre><code>this is indented code
|
|
||||||
</code></pre>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders code fence 1`] = `
|
|
||||||
"<pre><code class=\\"language-javascript\\">this is code
|
|
||||||
</code></pre>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders code marks 1`] = `"<p>this is <code>inline code</code> text</p>"`;
|
|
||||||
|
|
||||||
exports[`renders headings 1`] = `
|
|
||||||
"<h1>Heading 1</h1>
|
|
||||||
<h2>Heading 2</h2>
|
|
||||||
<h3>Heading 3</h3>
|
|
||||||
<h4>Heading 4</h4>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders highlight marks 1`] = `"<p>this is <span class=\\"highlight\\">highlighted</span> text</p>"`;
|
|
||||||
|
|
||||||
exports[`renders horizontal rule 1`] = `"<hr>"`;
|
|
||||||
|
|
||||||
exports[`renders image 1`] = `"<p><img src=\\"https://lorempixel.com/200/200\\" alt=\\"caption\\"></p>"`;
|
|
||||||
|
|
||||||
exports[`renders image with alignment 1`] = `"<p><img src=\\"https://lorempixel.com/200/200\\" alt=\\"caption\\" title=\\"left-40\\"></p>"`;
|
|
||||||
|
|
||||||
exports[`renders info notice 1`] = `
|
|
||||||
"<div class=\\"notice notice-info\\">
|
|
||||||
<p>content of notice</p>
|
|
||||||
</div>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders italic marks 1`] = `"<p>this is <em>italic</em> text</p>"`;
|
|
||||||
|
|
||||||
exports[`renders italic marks 2`] = `"<p>this is <em>also italic</em> text</p>"`;
|
|
||||||
|
|
||||||
exports[`renders link marks 1`] = `"<p>this is <a href=\\"https://www.example.com\\">linked</a> text</p>"`;
|
|
||||||
|
|
||||||
exports[`renders ordered list 1`] = `
|
|
||||||
"<ol>
|
|
||||||
<li>item one</li>
|
|
||||||
<li>item two</li>
|
|
||||||
</ol>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders ordered list 2`] = `
|
|
||||||
"<ol>
|
|
||||||
<li>item one</li>
|
|
||||||
<li>item two</li>
|
|
||||||
</ol>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders plain text as paragraph 1`] = `"<p>plain text</p>"`;
|
|
||||||
|
|
||||||
exports[`renders table 1`] = `
|
|
||||||
"<table>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<p>heading</p></th>
|
|
||||||
<th style=\\"text-align:center\\">
|
|
||||||
<p>centered</p></th>
|
|
||||||
<th style=\\"text-align:right\\">
|
|
||||||
<p>right aligned</p></th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p></p></td>
|
|
||||||
<td style=\\"text-align:center\\">
|
|
||||||
<p>center</p></td>
|
|
||||||
<td style=\\"text-align:right\\">
|
|
||||||
<p></p></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<p></p></td>
|
|
||||||
<td style=\\"text-align:center\\">
|
|
||||||
<p></p></td>
|
|
||||||
<td style=\\"text-align:right\\">
|
|
||||||
<p>bottom r</p></td>
|
|
||||||
</tr>
|
|
||||||
</table>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders template placeholder marks 1`] = `"<p>this is <span class=\\"placeholder\\">a placeholder</span></p>"`;
|
|
||||||
|
|
||||||
exports[`renders tip notice 1`] = `
|
|
||||||
"<div class=\\"notice notice-tip\\">
|
|
||||||
<p>content of notice</p>
|
|
||||||
</div>"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`renders underline marks 1`] = `"<p>this is <underline>underlined</underline> text</p>"`;
|
|
||||||
|
|
||||||
exports[`renders underline marks 2`] = `"<p>this is <s>strikethrough</s> text</p>"`;
|
|
||||||
|
|
||||||
exports[`renders warning notice 1`] = `
|
|
||||||
"<div class=\\"notice notice-warning\\">
|
|
||||||
<p>content of notice</p>
|
|
||||||
</div>"
|
|
||||||
`;
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Schema } from "prosemirror-model";
|
import { Schema } from "prosemirror-model";
|
||||||
import ExtensionManager from "@shared/editor/lib/ExtensionManager";
|
import ExtensionManager from "@shared/editor/lib/ExtensionManager";
|
||||||
import fullPackage from "@shared/editor/packages/full";
|
import fullPackage from "@shared/editor/packages/full";
|
||||||
import render from "./renderToHtml";
|
|
||||||
|
|
||||||
const extensions = new ExtensionManager(fullPackage);
|
const extensions = new ExtensionManager(fullPackage);
|
||||||
|
|
||||||
@@ -16,6 +15,3 @@ export const parser = extensions.parser({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const serializer = extensions.serializer();
|
export const serializer = extensions.serializer();
|
||||||
|
|
||||||
export const renderToHtml = (markdown: string): string =>
|
|
||||||
render(markdown, extensions.rulePlugins);
|
|
||||||
|
|||||||
@@ -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(``)
|
|
||||||
).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders image with alignment", () => {
|
|
||||||
expect(
|
|
||||||
renderToHtml(``)
|
|
||||||
).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();
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { updateYFragment } from "@getoutline/y-prosemirror";
|
|
||||||
import removeMarkdown from "@tommoor/remove-markdown";
|
import removeMarkdown from "@tommoor/remove-markdown";
|
||||||
import invariant from "invariant";
|
import invariant from "invariant";
|
||||||
import { compact, find, map, uniq } from "lodash";
|
import { compact, find, map, uniq } from "lodash";
|
||||||
@@ -34,14 +33,12 @@ import {
|
|||||||
} from "sequelize-typescript";
|
} from "sequelize-typescript";
|
||||||
import MarkdownSerializer from "slate-md-serializer";
|
import MarkdownSerializer from "slate-md-serializer";
|
||||||
import isUUID from "validator/lib/isUUID";
|
import isUUID from "validator/lib/isUUID";
|
||||||
import * as Y from "yjs";
|
|
||||||
import { DateFilter } from "@shared/types";
|
import { DateFilter } from "@shared/types";
|
||||||
import getTasks from "@shared/utils/getTasks";
|
import getTasks from "@shared/utils/getTasks";
|
||||||
import parseTitle from "@shared/utils/parseTitle";
|
import parseTitle from "@shared/utils/parseTitle";
|
||||||
import unescape from "@shared/utils/unescape";
|
import unescape from "@shared/utils/unescape";
|
||||||
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
import { SLUG_URL_REGEX } from "@shared/utils/urlHelpers";
|
||||||
import { DocumentValidation } from "@shared/validations";
|
import { DocumentValidation } from "@shared/validations";
|
||||||
import { parser } from "@server/editor";
|
|
||||||
import slugify from "@server/utils/slugify";
|
import slugify from "@server/utils/slugify";
|
||||||
import Backlink from "./Backlink";
|
import Backlink from "./Backlink";
|
||||||
import Collection from "./Collection";
|
import Collection from "./Collection";
|
||||||
@@ -482,7 +479,7 @@ class Document extends ParanoidModel {
|
|||||||
query: string,
|
query: string,
|
||||||
options: SearchOptions = {}
|
options: SearchOptions = {}
|
||||||
): Promise<SearchResponse> {
|
): Promise<SearchResponse> {
|
||||||
const wildcardQuery = `${escape(query)}:*`;
|
const wildcardQuery = `${escapeQuery(query)}:*`;
|
||||||
const {
|
const {
|
||||||
snippetMinWords = 20,
|
snippetMinWords = 20,
|
||||||
snippetMaxWords = 30,
|
snippetMaxWords = 30,
|
||||||
@@ -610,7 +607,7 @@ class Document extends ParanoidModel {
|
|||||||
limit = 15,
|
limit = 15,
|
||||||
offset = 0,
|
offset = 0,
|
||||||
} = options;
|
} = options;
|
||||||
const wildcardQuery = `${escape(query)}:*`;
|
const wildcardQuery = `${escapeQuery(query)}:*`;
|
||||||
|
|
||||||
// Ensure we're filtering by the users accessible collections. If
|
// Ensure we're filtering by the users accessible collections. If
|
||||||
// collectionId is passed as an option it is assumed that the authorization
|
// collectionId is passed as an option it is assumed that the authorization
|
||||||
@@ -731,38 +728,6 @@ class Document extends ParanoidModel {
|
|||||||
|
|
||||||
// instance methods
|
// 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 = () => {
|
migrateVersion = () => {
|
||||||
let migrated = false;
|
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
|
// replace "\" with escaped "\\" because sequelize.escape doesn't do it
|
||||||
// https://github.com/sequelize/sequelize/issues/2950
|
// https://github.com/sequelize/sequelize/issues/2950
|
||||||
return Document.sequelize!.escape(query).replace(/\\/g, "\\\\");
|
return Document.sequelize!.escape(query).replace(/\\/g, "\\\\");
|
||||||
|
|||||||
154
server/models/helpers/DocumentHelper.tsx
Normal file
154
server/models/helpers/DocumentHelper.tsx
Normal file
@@ -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(
|
||||||
|
<ThemeProvider theme={light}>
|
||||||
|
<>
|
||||||
|
<GlobalStyles />
|
||||||
|
<Centered>
|
||||||
|
<h1>{document.title}</h1>
|
||||||
|
<EditorContainer rtl={false}>
|
||||||
|
<div id="content" className="ProseMirror"></div>
|
||||||
|
</EditorContainer>
|
||||||
|
</Centered>
|
||||||
|
</>
|
||||||
|
</ThemeProvider>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
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(`<!DOCTYPE html>${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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
SearchQuery,
|
SearchQuery,
|
||||||
Event,
|
Event,
|
||||||
} from "@server/models";
|
} from "@server/models";
|
||||||
|
import DocumentHelper from "@server/models/helpers/DocumentHelper";
|
||||||
import {
|
import {
|
||||||
buildShare,
|
buildShare,
|
||||||
buildCollection,
|
buildCollection,
|
||||||
@@ -462,7 +463,22 @@ describe("#documents.export", () => {
|
|||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(res.status).toEqual(200);
|
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 () => {
|
it("should return archived document", async () => {
|
||||||
@@ -476,7 +492,7 @@ describe("#documents.export", () => {
|
|||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(res.status).toEqual(200);
|
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 () => {
|
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();
|
const body = await res.json();
|
||||||
expect(res.status).toEqual(200);
|
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 () => {
|
it("should return document from shareId without token", async () => {
|
||||||
@@ -526,7 +542,7 @@ describe("#documents.export", () => {
|
|||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(res.status).toEqual(200);
|
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 () => {
|
it("should not return document from revoked shareId", async () => {
|
||||||
@@ -576,7 +592,7 @@ describe("#documents.export", () => {
|
|||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(res.status).toEqual(200);
|
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 () => {
|
it("should return draft document from shareId with token", async () => {
|
||||||
@@ -596,7 +612,7 @@ describe("#documents.export", () => {
|
|||||||
});
|
});
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(res.status).toEqual(200);
|
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 () => {
|
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();
|
const body = await res.json();
|
||||||
expect(res.status).toEqual(200);
|
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 () => {
|
it("should require authorization without token", async () => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import invariant from "invariant";
|
import invariant from "invariant";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
|
import mime from "mime-types";
|
||||||
import { Op, ScopeOptions, WhereOptions } from "sequelize";
|
import { Op, ScopeOptions, WhereOptions } from "sequelize";
|
||||||
import { subtractDate } from "@shared/utils/date";
|
import { subtractDate } from "@shared/utils/date";
|
||||||
import documentCreator from "@server/commands/documentCreator";
|
import documentCreator from "@server/commands/documentCreator";
|
||||||
@@ -27,6 +28,7 @@ import {
|
|||||||
User,
|
User,
|
||||||
View,
|
View,
|
||||||
} from "@server/models";
|
} from "@server/models";
|
||||||
|
import DocumentHelper from "@server/models/helpers/DocumentHelper";
|
||||||
import { authorize, cannot } from "@server/policies";
|
import { authorize, cannot } from "@server/policies";
|
||||||
import {
|
import {
|
||||||
presentCollection,
|
presentCollection,
|
||||||
@@ -439,14 +441,46 @@ router.post(
|
|||||||
async (ctx) => {
|
async (ctx) => {
|
||||||
const { id, shareId } = ctx.body;
|
const { id, shareId } = ctx.body;
|
||||||
assertPresent(id || shareId, "id or shareId is required");
|
assertPresent(id || shareId, "id or shareId is required");
|
||||||
|
|
||||||
const { user } = ctx.state;
|
const { user } = ctx.state;
|
||||||
|
const accept = ctx.request.headers["accept"];
|
||||||
|
|
||||||
const { document } = await documentLoader({
|
const { document } = await documentLoader({
|
||||||
id,
|
id,
|
||||||
shareId,
|
shareId,
|
||||||
user,
|
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 = {
|
ctx.body = {
|
||||||
data: document.toMarkdown(),
|
data: content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Logger from "@server/logging/Logger";
|
|||||||
import Attachment from "@server/models/Attachment";
|
import Attachment from "@server/models/Attachment";
|
||||||
import Collection from "@server/models/Collection";
|
import Collection from "@server/models/Collection";
|
||||||
import Document from "@server/models/Document";
|
import Document from "@server/models/Document";
|
||||||
|
import DocumentHelper from "@server/models/helpers/DocumentHelper";
|
||||||
import { NavigationNode } from "~/types";
|
import { NavigationNode } from "~/types";
|
||||||
import { deserializeFilename, serializeFilename } from "./fs";
|
import { deserializeFilename, serializeFilename } from "./fs";
|
||||||
import parseAttachmentIds from "./parseAttachmentIds";
|
import parseAttachmentIds from "./parseAttachmentIds";
|
||||||
@@ -36,7 +37,7 @@ async function addDocumentTreeToArchive(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = document.toMarkdown();
|
let text = DocumentHelper.toMarkdown(document);
|
||||||
const attachments = await Attachment.findAll({
|
const attachments = await Attachment.findAll({
|
||||||
where: {
|
where: {
|
||||||
teamId: document.teamId,
|
teamId: document.teamId,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function assertArray(
|
|||||||
|
|
||||||
export const assertIn = (
|
export const assertIn = (
|
||||||
value: string,
|
value: string,
|
||||||
options: (string | undefined | null)[],
|
options: Primitive[],
|
||||||
message?: string
|
message?: string
|
||||||
) => {
|
) => {
|
||||||
if (!options.includes(value)) {
|
if (!options.includes(value)) {
|
||||||
|
|||||||
8
shared/editor/components/Styles.ts
Normal file
8
shared/editor/components/Styles.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
import style, { Props } from "../../styles/editor";
|
||||||
|
|
||||||
|
const EditorContainer = styled.div<Props>`
|
||||||
|
${style};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default EditorContainer;
|
||||||
@@ -33,12 +33,16 @@ export default class CheckboxItem extends Node {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: (node) => {
|
toDOM: (node) => {
|
||||||
const input = document.createElement("span");
|
const checked = node.attrs.checked.toString();
|
||||||
input.tabIndex = -1;
|
let input;
|
||||||
input.className = "checkbox";
|
if (typeof document !== "undefined") {
|
||||||
input.setAttribute("aria-checked", node.attrs.checked.toString());
|
input = document.createElement("span");
|
||||||
input.setAttribute("role", "checkbox");
|
input.tabIndex = -1;
|
||||||
input.addEventListener("click", this.handleClick);
|
input.className = "checkbox";
|
||||||
|
input.setAttribute("aria-checked", checked);
|
||||||
|
input.setAttribute("role", "checkbox");
|
||||||
|
input.addEventListener("click", this.handleClick);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"li",
|
"li",
|
||||||
@@ -51,7 +55,9 @@ export default class CheckboxItem extends Node {
|
|||||||
{
|
{
|
||||||
contentEditable: "false",
|
contentEditable: "false",
|
||||||
},
|
},
|
||||||
input,
|
...(input
|
||||||
|
? [input]
|
||||||
|
: [["span", { class: "checkbox", "aria-checked": checked }]]),
|
||||||
],
|
],
|
||||||
["div", 0],
|
["div", 0],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -122,44 +122,53 @@ export default class CodeFence extends Node {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: (node) => {
|
toDOM: (node) => {
|
||||||
const button = document.createElement("button");
|
let actions;
|
||||||
button.innerText = this.options.dictionary.copy;
|
if (typeof document !== "undefined") {
|
||||||
button.type = "button";
|
const button = document.createElement("button");
|
||||||
button.addEventListener("click", this.handleCopyToClipboard);
|
button.innerText = this.options.dictionary.copy;
|
||||||
|
button.type = "button";
|
||||||
|
button.addEventListener("click", this.handleCopyToClipboard);
|
||||||
|
|
||||||
const select = document.createElement("select");
|
const select = document.createElement("select");
|
||||||
select.addEventListener("change", this.handleLanguageChange);
|
select.addEventListener("change", this.handleLanguageChange);
|
||||||
|
|
||||||
const actions = document.createElement("div");
|
actions = document.createElement("div");
|
||||||
actions.className = "code-actions";
|
actions.className = "code-actions";
|
||||||
actions.appendChild(select);
|
actions.appendChild(select);
|
||||||
actions.appendChild(button);
|
actions.appendChild(button);
|
||||||
|
|
||||||
this.languageOptions.forEach(([key, label]) => {
|
this.languageOptions.forEach(([key, label]) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
const value = key === "none" ? "" : key;
|
const value = key === "none" ? "" : key;
|
||||||
option.value = value;
|
option.value = value;
|
||||||
option.innerText = label;
|
option.innerText = label;
|
||||||
option.selected = node.attrs.language === value;
|
option.selected = node.attrs.language === value;
|
||||||
select.appendChild(option);
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
// For the Mermaid language we add an extra button to toggle between
|
// For the Mermaid language we add an extra button to toggle between
|
||||||
// source code and a rendered diagram view.
|
// source code and a rendered diagram view.
|
||||||
if (node.attrs.language === "mermaidjs") {
|
if (node.attrs.language === "mermaidjs") {
|
||||||
const showSourceButton = document.createElement("button");
|
const showSourceButton = document.createElement("button");
|
||||||
showSourceButton.innerText = this.options.dictionary.showSource;
|
showSourceButton.innerText = this.options.dictionary.showSource;
|
||||||
showSourceButton.type = "button";
|
showSourceButton.type = "button";
|
||||||
showSourceButton.classList.add("show-source-button");
|
showSourceButton.classList.add("show-source-button");
|
||||||
showSourceButton.addEventListener("click", this.handleToggleDiagram);
|
showSourceButton.addEventListener(
|
||||||
actions.prepend(showSourceButton);
|
"click",
|
||||||
|
this.handleToggleDiagram
|
||||||
|
);
|
||||||
|
actions.prepend(showSourceButton);
|
||||||
|
|
||||||
const showDiagramButton = document.createElement("button");
|
const showDiagramButton = document.createElement("button");
|
||||||
showDiagramButton.innerText = this.options.dictionary.showDiagram;
|
showDiagramButton.innerText = this.options.dictionary.showDiagram;
|
||||||
showDiagramButton.type = "button";
|
showDiagramButton.type = "button";
|
||||||
showDiagramButton.classList.add("show-digram-button");
|
showDiagramButton.classList.add("show-digram-button");
|
||||||
showDiagramButton.addEventListener("click", this.handleToggleDiagram);
|
showDiagramButton.addEventListener(
|
||||||
actions.prepend(showDiagramButton);
|
"click",
|
||||||
|
this.handleToggleDiagram
|
||||||
|
);
|
||||||
|
actions.prepend(showDiagramButton);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -168,7 +177,7 @@ export default class CodeFence extends Node {
|
|||||||
class: "code-block",
|
class: "code-block",
|
||||||
"data-language": node.attrs.language,
|
"data-language": node.attrs.language,
|
||||||
},
|
},
|
||||||
["div", { contentEditable: "false" }, actions],
|
...(actions ? [["div", { contentEditable: "false" }, actions]] : []),
|
||||||
["pre", ["code", { spellCheck: "false" }, 0]],
|
["pre", ["code", { spellCheck: "false" }, 0]],
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,23 +50,28 @@ export default class Heading extends Node {
|
|||||||
contentElement: ".heading-content",
|
contentElement: ".heading-content",
|
||||||
})),
|
})),
|
||||||
toDOM: (node) => {
|
toDOM: (node) => {
|
||||||
const anchor = document.createElement("button");
|
let anchor, fold;
|
||||||
anchor.innerText = "#";
|
if (typeof document !== "undefined") {
|
||||||
anchor.type = "button";
|
anchor = document.createElement("button");
|
||||||
anchor.className = "heading-anchor";
|
anchor.innerText = "#";
|
||||||
anchor.addEventListener("click", (event) => this.handleCopyLink(event));
|
anchor.type = "button";
|
||||||
|
anchor.className = "heading-anchor";
|
||||||
|
anchor.addEventListener("click", (event) =>
|
||||||
|
this.handleCopyLink(event)
|
||||||
|
);
|
||||||
|
|
||||||
const fold = document.createElement("button");
|
fold = document.createElement("button");
|
||||||
fold.innerText = "";
|
fold.innerText = "";
|
||||||
fold.innerHTML =
|
fold.innerHTML =
|
||||||
'<svg fill="currentColor" width="12" height="24" viewBox="6 0 12 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.23823905,10.6097108 L11.207376,14.4695888 L11.207376,14.4695888 C11.54411,14.907343 12.1719566,14.989236 12.6097108,14.652502 C12.6783439,14.5997073 12.7398293,14.538222 12.792624,14.4695888 L15.761761,10.6097108 L15.761761,10.6097108 C16.0984949,10.1719566 16.0166019,9.54410997 15.5788477,9.20737601 C15.4040391,9.07290785 15.1896811,9 14.969137,9 L9.03086304,9 L9.03086304,9 C8.47857829,9 8.03086304,9.44771525 8.03086304,10 C8.03086304,10.2205442 8.10377089,10.4349022 8.23823905,10.6097108 Z" /></svg>';
|
'<svg fill="currentColor" width="12" height="24" viewBox="6 0 12 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.23823905,10.6097108 L11.207376,14.4695888 L11.207376,14.4695888 C11.54411,14.907343 12.1719566,14.989236 12.6097108,14.652502 C12.6783439,14.5997073 12.7398293,14.538222 12.792624,14.4695888 L15.761761,10.6097108 L15.761761,10.6097108 C16.0984949,10.1719566 16.0166019,9.54410997 15.5788477,9.20737601 C15.4040391,9.07290785 15.1896811,9 14.969137,9 L9.03086304,9 L9.03086304,9 C8.47857829,9 8.03086304,9.44771525 8.03086304,10 C8.03086304,10.2205442 8.10377089,10.4349022 8.23823905,10.6097108 Z" /></svg>';
|
||||||
fold.type = "button";
|
fold.type = "button";
|
||||||
fold.className = `heading-fold ${
|
fold.className = `heading-fold ${
|
||||||
node.attrs.collapsed ? "collapsed" : ""
|
node.attrs.collapsed ? "collapsed" : ""
|
||||||
}`;
|
}`;
|
||||||
fold.addEventListener("mousedown", (event) =>
|
fold.addEventListener("mousedown", (event) =>
|
||||||
this.handleFoldContent(event)
|
this.handleFoldContent(event)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
`h${node.attrs.level + (this.options.offset || 0)}`,
|
`h${node.attrs.level + (this.options.offset || 0)}`,
|
||||||
@@ -78,8 +83,7 @@ export default class Heading extends Node {
|
|||||||
node.attrs.collapsed ? "collapsed" : ""
|
node.attrs.collapsed ? "collapsed" : ""
|
||||||
}`,
|
}`,
|
||||||
},
|
},
|
||||||
anchor,
|
...(anchor ? [anchor, fold] : []),
|
||||||
fold,
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"span",
|
"span",
|
||||||
|
|||||||
@@ -52,40 +52,43 @@ export default class Notice extends Node {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: (node) => {
|
toDOM: (node) => {
|
||||||
const select = document.createElement("select");
|
let icon, actions;
|
||||||
select.addEventListener("change", this.handleStyleChange);
|
if (typeof document !== "undefined") {
|
||||||
|
const select = document.createElement("select");
|
||||||
|
select.addEventListener("change", this.handleStyleChange);
|
||||||
|
|
||||||
this.styleOptions.forEach(([key, label]) => {
|
this.styleOptions.forEach(([key, label]) => {
|
||||||
const option = document.createElement("option");
|
const option = document.createElement("option");
|
||||||
option.value = key;
|
option.value = key;
|
||||||
option.innerText = label;
|
option.innerText = label;
|
||||||
option.selected = node.attrs.style === key;
|
option.selected = node.attrs.style === key;
|
||||||
select.appendChild(option);
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
const actions = document.createElement("div");
|
actions = document.createElement("div");
|
||||||
actions.className = "notice-actions";
|
actions.className = "notice-actions";
|
||||||
actions.appendChild(select);
|
actions.appendChild(select);
|
||||||
|
|
||||||
let component;
|
let component;
|
||||||
|
|
||||||
if (node.attrs.style === "tip") {
|
if (node.attrs.style === "tip") {
|
||||||
component = <StarredIcon color="currentColor" />;
|
component = <StarredIcon color="currentColor" />;
|
||||||
} else if (node.attrs.style === "warning") {
|
} else if (node.attrs.style === "warning") {
|
||||||
component = <WarningIcon color="currentColor" />;
|
component = <WarningIcon color="currentColor" />;
|
||||||
} else {
|
} else {
|
||||||
component = <InfoIcon color="currentColor" />;
|
component = <InfoIcon color="currentColor" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
icon = document.createElement("div");
|
||||||
|
icon.className = "icon";
|
||||||
|
ReactDOM.render(component, icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = document.createElement("div");
|
|
||||||
icon.className = "icon";
|
|
||||||
ReactDOM.render(component, icon);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"div",
|
"div",
|
||||||
{ class: `notice-block ${node.attrs.style}` },
|
{ class: `notice-block ${node.attrs.style}` },
|
||||||
icon,
|
...(icon ? [icon] : []),
|
||||||
["div", { contentEditable: "false" }, actions],
|
["div", { contentEditable: "false" }, ...(actions ? [actions] : [])],
|
||||||
["div", { class: "content" }, 0],
|
["div", { class: "content" }, 0],
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
"Subscribed to document notifications": "Subscribed to document notifications",
|
"Subscribed to document notifications": "Subscribed to document notifications",
|
||||||
"Unsubscribe": "Unsubscribe",
|
"Unsubscribe": "Unsubscribe",
|
||||||
"Unsubscribed from document notifications": "Unsubscribed from document notifications",
|
"Unsubscribed from document notifications": "Unsubscribed from document notifications",
|
||||||
|
"HTML": "HTML",
|
||||||
|
"Markdown": "Markdown",
|
||||||
"Download": "Download",
|
"Download": "Download",
|
||||||
"Download document": "Download document",
|
"Download document": "Download document",
|
||||||
"Duplicate": "Duplicate",
|
"Duplicate": "Duplicate",
|
||||||
|
|||||||
1304
shared/styles/editor.ts
Normal file
1304
shared/styles/editor.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
import { createGlobalStyle } from "styled-components";
|
import { createGlobalStyle } from "styled-components";
|
||||||
import styledNormalize from "styled-normalize";
|
import styledNormalize from "styled-normalize";
|
||||||
import { breakpoints, depths } from "@shared/styles";
|
import { breakpoints, depths } from ".";
|
||||||
|
|
||||||
export default createGlobalStyle`
|
export default createGlobalStyle`
|
||||||
${styledNormalize}
|
${styledNormalize}
|
||||||
66
yarn.lock
66
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"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||||
|
|
||||||
acorn@^8.5.0:
|
acorn@^8.5.0, acorn@^8.7.1:
|
||||||
version "8.8.0"
|
version "8.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
|
||||||
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
|
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"
|
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
|
||||||
integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==
|
integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==
|
||||||
|
|
||||||
data-urls@^3.0.1:
|
data-urls@^3.0.1, data-urls@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
|
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
|
||||||
integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==
|
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"
|
resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
|
||||||
integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==
|
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:
|
entities@~3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
|
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"
|
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||||
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
|
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
|
||||||
|
|
||||||
https-proxy-agent@^5.0.0:
|
https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1:
|
||||||
version "5.0.0"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
|
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6"
|
||||||
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
|
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base "6"
|
agent-base "6"
|
||||||
debug "4"
|
debug "4"
|
||||||
@@ -9617,6 +9622,39 @@ jsdom@^19.0.0:
|
|||||||
ws "^8.2.3"
|
ws "^8.2.3"
|
||||||
xml-name-validator "^4.0.0"
|
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:
|
jsesc@^2.5.1:
|
||||||
version "2.5.2"
|
version "2.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
|
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"
|
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||||
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
|
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:
|
parseqs@0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
|
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
|
||||||
@@ -13224,6 +13269,13 @@ saxes@^5.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xmlchars "^2.2.0"
|
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:
|
scheduler@^0.19.1:
|
||||||
version "0.19.1"
|
version "0.19.1"
|
||||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
|
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"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b"
|
||||||
integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==
|
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"
|
version "8.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0"
|
||||||
integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==
|
integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==
|
||||||
|
|||||||
Reference in New Issue
Block a user