Skip to content

Commit

Permalink
feat: Add mermaidjs integration (outline#3679)
Browse files Browse the repository at this point in the history
* feat: Add mermaidjs integration (outline#3523)

* Add mermaidjs to dependencies and CodeFenceNode

* Fix diagram id for mermaidjs diagrams

* Fix typescript compiler errors on mermaid integration

* Fix id generation for mermaid diagrams

* Refactor mermaidjs integration into prosemirror plugin

* Remove unnecessary class attribute in mermaidjs integration

* Change mermaidjs label to singular

* Change decorator.inline to decorator.node for mermaid diagram id

* Fix diagram toggle state

* Add border and background to mermaid diagrams

* Stop mermaidjs from overwriting fontFamily inside diagrams

* Add stable diagramId to mermaid diagrams

* Separate text for hide/show diagram
Use uuid as diagramId, avoid storing in state
Fix cursor on diagrams

* fix: Base diagram visibility off presence of source

* fix: More cases where our font-family is ignored

* Disable HTML labels

* fix: Button styling – not technically required but now we have a third button this felt all the more needed

closes outline#3116

* named chunks

* Upgrade mermaid 9.1.3

Co-authored-by: Jan Niklas Richter <5812215+ArcticXWolf@users.noreply.github.com>
  • Loading branch information
2 people authored and Avalanche committed Jul 2, 2022
1 parent 9415218 commit c3652f8
Show file tree
Hide file tree
Showing 9 changed files with 975 additions and 283 deletions.
86 changes: 77 additions & 9 deletions app/editor/components/Styles.ts
@@ -1,5 +1,5 @@
/* eslint-disable no-irregular-whitespace */
import { lighten, transparentize } from "polished";
import { darken, lighten, transparentize } from "polished";
import styled from "styled-components";

const EditorStyles = styled.div<{
Expand Down Expand Up @@ -773,20 +773,45 @@ const EditorStyles = styled.div<{
select,
button {
background: ${(props) => props.theme.background};
color: ${(props) => props.theme.text};
border-width: 1px;
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;
border-radius: 4px;
padding: 2px 4px;
height: 18px;
&::-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;
}
}
button {
padding: 2px 4px;
select {
background-image: url('data:image/svg+xml;utf8,<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M9.03087 9C8.20119 9 7.73238 9.95209 8.23824 10.6097L11.2074 14.4696C11.6077 14.99 12.3923 14.99 12.7926 14.4696L15.7618 10.6097C16.2676 9.95209 15.7988 9 14.9691 9L9.03087 9Z" fill="currentColor"/> </svg>');
background-repeat: no-repeat;
background-position: center right;
padding-right: 20px;
}
&:focus-within,
&:hover {
select {
display: ${(props) => (props.readOnly ? "none" : "inline")};
Expand All @@ -803,6 +828,49 @@ const EditorStyles = styled.div<{
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 {
Expand Down
3 changes: 3 additions & 0 deletions app/hooks/useDictionary.ts
Expand Up @@ -18,6 +18,7 @@ export default function useDictionary() {
codeBlock: t("Code block"),
codeCopied: t("Copied to clipboard"),
codeInline: t("Code"),
copy: t("Copy"),
createLink: t("Create link"),
createLinkError: t("Sorry, an error occurred creating the link"),
createNewDoc: t("Create a new doc"),
Expand Down Expand Up @@ -69,6 +70,8 @@ export default function useDictionary() {
table: t("Table"),
tip: t("Tip"),
tipNotice: t("Tip notice"),
showDiagram: t("Show diagram"),
showSource: t("Show source"),
warning: t("Warning"),
warningNotice: t("Warning notice"),
insertDate: t("Current date"),
Expand Down
5 changes: 4 additions & 1 deletion app/utils/polyfills.ts
Expand Up @@ -8,7 +8,10 @@ export async function loadPolyfills() {

if (!supportsResizeObserver()) {
polyfills.push(
import("@juggle/resize-observer").then((module) => {
import(
/* webpackChunkName: "resize-observer" */
"@juggle/resize-observer"
).then((module) => {
window.ResizeObserver = module.ResizeObserver;
})
);
Expand Down
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -67,6 +67,7 @@
"@theo.gravity/datadog-apm": "2.1.0",
"@tippy.js/react": "^2.2.2",
"@tommoor/remove-markdown": "^0.3.2",
"@types/mermaid": "^8.2.9",
"autotrack": "^2.4.1",
"aws-sdk": "^2.1044.0",
"babel-plugin-lodash": "^3.3.4",
Expand Down Expand Up @@ -126,6 +127,7 @@
"markdown-it": "^12.3.2",
"markdown-it-container": "^3.0.0",
"markdown-it-emoji": "^2.0.0",
"mermaid": "9.1.3",
"mime-types": "^2.1.35",
"mobx": "^4.15.4",
"mobx-react": "^6.3.1",
Expand Down Expand Up @@ -328,6 +330,7 @@
"yarn-deduplicate": "^3.1.0"
},
"resolutions": {
"d3": "^7.0.0",
"node-fetch": "^2.6.7",
"socket.io-parser": "^3.4.0",
"prosemirror-transform": "1.2.5",
Expand Down
56 changes: 52 additions & 4 deletions shared/editor/nodes/CodeFence.ts
Expand Up @@ -38,6 +38,7 @@ import { Dictionary } from "~/hooks/useDictionary";

import toggleBlockType from "../commands/toggleBlockType";
import { MarkdownSerializerState } from "../lib/markdown/serializer";
import Mermaid from "../plugins/Mermaid";
import Prism, { LANGUAGES } from "../plugins/Prism";
import isInCode from "../queries/isInCode";
import { Dispatch } from "../types";
Expand Down Expand Up @@ -114,7 +115,7 @@ export default class CodeFence extends Node {
],
toDOM: (node) => {
const button = document.createElement("button");
button.innerText = "Copy";
button.innerText = this.options.dictionary.copy;
button.type = "button";
button.addEventListener("click", this.handleCopyToClipboard);

Expand All @@ -135,9 +136,30 @@ export default class CodeFence extends Node {
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);

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 [
"div",
{ class: "code-block", "data-language": node.attrs.language },
{
class: "code-block",
"data-language": node.attrs.language,
},
["div", { contentEditable: "false" }, actions],
["pre", ["code", { spellCheck: "false" }, 0]],
];
Expand Down Expand Up @@ -222,20 +244,46 @@ export default class CodeFence extends Node {

if (result) {
const language = element.value;

const transaction = tr
.setSelection(Selection.near(view.state.doc.resolve(result.inside)))
.setNodeMarkup(result.inside, undefined, {
language,
});

view.dispatch(transaction);

localStorage?.setItem(PERSISTENCE_KEY, language);
}
};

handleToggleDiagram = (event: InputEvent) => {
const { view } = this.editor;
const { tr } = view.state;
const element = event.currentTarget;
if (!(element instanceof HTMLButtonElement)) {
return;
}

const { top, left } = element.getBoundingClientRect();
const result = view.posAtCoords({ top, left });

if (!result) {
return;
}

const diagramId = element
.closest(".code-block")
?.getAttribute("data-diagram-id");
if (!diagramId) {
return;
}

const transaction = tr.setMeta("mermaid", { toggleDiagram: diagramId });
view.dispatch(transaction);
};

get plugins() {
return [Prism({ name: this.name })];
return [Prism({ name: this.name }), Mermaid({ name: this.name })];
}

inputRules({ type }: { type: NodeType }) {
Expand Down

0 comments on commit c3652f8

Please sign in to comment.