import type Token from "markdown-it/lib/token.mjs"; import { WarningIcon, InfoIcon, StarredIcon, DoneIcon } from "outline-icons"; import { wrappingInputRule } from "prosemirror-inputrules"; import type { NodeSpec, Node as ProsemirrorNode, NodeType, } from "prosemirror-model"; import type { Command, EditorState, Transaction } from "prosemirror-state"; import * as React from "react"; import ReactDOM from "react-dom"; import type { Primitive } from "utility-types"; import toggleWrap from "../commands/toggleWrap"; import type { MarkdownSerializerState } from "../lib/markdown/serializer"; import noticesRule from "../rules/notices"; import Node from "./Node"; export enum NoticeTypes { Info = "info", Success = "success", Tip = "tip", Warning = "warning", } export default class Notice extends Node { get name() { return "container_notice"; } get rulePlugins() { return [noticesRule]; } get schema(): NodeSpec { return { attrs: { style: { default: NoticeTypes.Info, }, }, content: "(list | blockquote | hr | paragraph | heading | code_block | code_fence | attachment)+", group: "block", defining: true, draggable: true, parseDOM: [ { tag: "div.notice-block", preserveWhitespace: "full", contentElement: (node: HTMLDivElement) => node.querySelector("div.content") || node, getAttrs: (dom: HTMLDivElement) => ({ style: dom.className.includes(NoticeTypes.Tip) ? NoticeTypes.Tip : dom.className.includes(NoticeTypes.Warning) ? NoticeTypes.Warning : dom.className.includes(NoticeTypes.Success) ? NoticeTypes.Success : undefined, }), }, // Quill editor parsing { tag: "div.ql-hint", preserveWhitespace: "full", getAttrs: (dom: HTMLDivElement) => ({ style: dom.dataset.hint, }), }, // GitBook parsing { tag: "div.alert.theme-admonition", preserveWhitespace: "full", getAttrs: (dom: HTMLDivElement) => ({ style: dom.className.includes(NoticeTypes.Warning) ? NoticeTypes.Warning : dom.className.includes(NoticeTypes.Success) ? NoticeTypes.Success : undefined, }), }, // Confluence parsing { tag: "div.confluence-information-macro", preserveWhitespace: "full", getAttrs: (dom: HTMLDivElement) => ({ style: dom.className.includes("confluence-information-macro-tip") ? NoticeTypes.Success : dom.className.includes("confluence-information-macro-note") ? NoticeTypes.Tip : dom.className.includes("confluence-information-macro-warning") ? NoticeTypes.Warning : undefined, }), }, ], toDOM: (node) => { let icon; if (typeof document !== "undefined") { let component; if (node.attrs.style === NoticeTypes.Tip) { component = ; } else if (node.attrs.style === NoticeTypes.Warning) { component = ; } else if (node.attrs.style === NoticeTypes.Success) { component = ; } else { component = ; } icon = document.createElement("div"); icon.className = "icon"; ReactDOM.render(component, icon); } return [ "div", { class: `notice-block ${node.attrs.style}` }, ...(icon ? [icon] : []), ["div", { class: "content" }, 0], ]; }, }; } commands({ type }: { type: NodeType }) { return { container_notice: (attrs: Record) => toggleWrap(type, attrs), info: (): Command => (state, dispatch) => this.handleStyleChange(state, dispatch, NoticeTypes.Info), warning: (): Command => (state, dispatch) => this.handleStyleChange(state, dispatch, NoticeTypes.Warning), success: (): Command => (state, dispatch) => this.handleStyleChange(state, dispatch, NoticeTypes.Success), tip: (): Command => (state, dispatch) => this.handleStyleChange(state, dispatch, NoticeTypes.Tip), }; } handleStyleChange = ( state: EditorState, dispatch: ((tr: Transaction) => void) | undefined, style: NoticeTypes ): boolean => { const { tr, selection } = state; const { $from } = selection; const node = $from.node(-1); if (node?.type.name === this.name) { if (dispatch) { const transaction = tr.setNodeMarkup($from.before(-1), undefined, { ...node.attrs, style, }); dispatch(transaction); } return true; } return false; }; inputRules({ type }: { type: NodeType }) { return [wrappingInputRule(/^:::$/, type)]; } toMarkdown(state: MarkdownSerializerState, node: ProsemirrorNode) { state.write("\n:::" + (node.attrs.style || "info") + "\n"); state.renderContent(node); state.ensureNewLine(); state.write(":::"); state.closeBlock(node); } parseMarkdown() { return { block: "container_notice", getAttrs: (tok: Token) => ({ style: tok.info }), }; } }