UNPKG

md-editor-rt

Version:

Markdown editor for react, developed in jsx and typescript, dark theme、beautify content by prettier、render articles directly、paste or clip the picture and upload it...

938 lines (937 loc) 22.4 kB
import { CompletionSource } from '@codemirror/autocomplete'; import { Compartment, Extension } from '@codemirror/state'; import { KeyBinding, EditorView } from '@codemirror/view'; import markdownit, { Token } from 'markdown-it'; import { CSSProperties, ComponentType, ReactElement, RefObject } from 'react'; import { IconName } from './components/Icon/Icon'; import { ToolDirective } from './utils/content-help'; declare global { interface Window { hljs: any; prettier: any; prettierPlugins: any; Cropper: any; screenfull: any; mermaid: any; katex: any; echarts: any; } } export interface ToolbarTips { bold?: string; underline?: string; italic?: string; strikeThrough?: string; title?: string; sub?: string; sup?: string; quote?: string; unorderedList?: string; orderedList?: string; task?: string; codeRow?: string; code?: string; link?: string; image?: string; table?: string; mermaid?: string; katex?: string; revoke?: string; next?: string; save?: string; prettier?: string; pageFullscreen?: string; fullscreen?: string; catalog?: string; preview?: string; previewOnly?: string; htmlPreview?: string; github?: string; '-'?: string; '='?: string; } export interface StaticTextDefaultValue { toolbarTips?: ToolbarTips; titleItem?: { h1?: string; h2?: string; h3?: string; h4?: string; h5?: string; h6?: string; }; imgTitleItem?: { link: string; upload: string; clip2upload: string; }; linkModalTips?: { linkTitle?: string; imageTitle?: string; descLabel?: string; descLabelPlaceHolder?: string; urlLabel?: string; urlLabelPlaceHolder?: string; buttonOK?: string; }; clipModalTips?: { title?: string; buttonUpload?: string; }; copyCode?: { text?: string; successTips?: string; failTips?: string; }; mermaid?: { flow?: string; sequence?: string; gantt?: string; class?: string; state?: string; pie?: string; relationship?: string; journey?: string; }; katex?: { inline: string; block: string; }; footer?: { markdownTotal: string; scrollAuto: string; }; } export interface StaticTextDefault { 'zh-CN': StaticTextDefaultValue; 'en-US': StaticTextDefaultValue; } export type StaticTextDefaultKey = keyof StaticTextDefault; export type ToolbarNames = keyof ToolbarTips | number; export type Footers = '=' | 'markdownTotal' | 'scrollSwitch' | number; export interface SettingType { pageFullscreen: boolean; fullscreen: boolean; preview: boolean; htmlPreview: boolean; previewOnly: boolean; } export interface HeadList { text: string; level: 1 | 2 | 3 | 4 | 5 | 6; active?: boolean; line: number; currentToken?: Token; nextToken?: Token; } export type Themes = 'light' | 'dark'; /** * 预览主题 * * @list ['default', 'github', 'vuepress', 'mk-cute', 'smart-blue', 'cyanosis'] */ export type PreviewThemes = string; export interface PreviewRendererProps { html: string; id: string; className: string; } export type PreviewRendererComponent = ComponentType<PreviewRendererProps>; /** * 自定义标题ID */ export type MdHeadingId = (options: { text: string; level: number; index: number; currentToken?: Token; nextToken?: Token; }) => string; export interface MdPreviewProps { value?: string; /** * @deprecated 5.0开始使用value代替 */ modelValue?: string; /** * input回调事件 */ onChange?: ChangeEvent; /** * 主题 * * @default 'light' */ theme?: Themes; /** * 外层类名 * * @default '' */ className?: string; /** * 预设语言名称 * * @default 'zh-CN' */ language?: string; /** * html变化事件 */ onHtmlChanged?: HtmlChangedEvent; /** * 获取目录结构 */ onGetCatalog?: GetCatalogEvent; /** * 5.x版本开始 editorId 的替换 * * @default 'md-editor-rt' */ id?: string; /** * 编辑器唯一标识 * * @default 'md-editor-rt' * @deprecated 5.x版本开始使用 id 替换 */ editorId?: string; /** * 预览中代码是否显示行号 * * @default false */ showCodeRowNumber?: boolean; /** * 预览内容样式 * * @default 'default' */ previewTheme?: PreviewThemes; /** * 标题的id生成方式 * * @default (text: string) => text */ mdHeadingId?: MdHeadingId; /** * 编辑器样式 */ style?: CSSProperties; /** * 不使用该mermaid * * @default false */ noMermaid?: boolean; /** * * 不能保证文本正确的情况,在marked编译md文本后通过该方法处理 * 推荐DOMPurify、sanitize-html * * @default (text: string) => text */ sanitize?: (html: string) => string; /** * 不使用katex * * @default false */ noKatex?: boolean; /** * 代码主题 * * @default 'atom' */ codeTheme?: string; /** * 复制代码格式化方法 * * @default (text) => text */ formatCopiedText?: (text: string) => string; /** * 是否禁用上传图片 * * @default false */ /** * 某些预览主题的代码模块背景是暗色系 * 将这个属性设置为true,会自动在该主题下的light模式下使用暗色系的代码风格 * * @default true */ codeStyleReverse?: boolean; /** * 需要自动调整的预览主题 * * @default ['default', 'mk-cute'] */ codeStyleReverseList?: Array<string>; /** * 是否启用代码高亮 */ noHighlight?: boolean; /** * 是否关闭编辑器默认的放大功能 */ noImgZoomIn?: boolean; /** * 自定义的图标 */ customIcon?: CustomIcon; /** * 转换生成的mermaid代码 * * @param html * @returns */ sanitizeMermaid?: (html: string) => Promise<string>; /** * 是否开启折叠代码功能 * 不开启会使用div标签替代details标签 * * @default true */ codeFoldable?: boolean; /** * 触发自动折叠代码的行数阈值 * * @default 30 */ autoFoldThreshold?: number; /** * 内容重新挂载事件 * * 相比起onHtmlChanged,onRemount会在重新挂载后触发 */ onRemount?: () => void; /** * 不使用 echarts */ noEcharts?: boolean; /** * 自定义渲染预览的组件 * * 组件会接收到 html、id、className,请确保将后两者应用到根节点 */ previewComponent?: PreviewRendererComponent; } export type TableShapeType = [number, number] | [number, number, number, number]; export interface EditorProps extends MdPreviewProps { /** * 保存事件 */ onSave?: SaveEvent; /** * 上传图片事件 */ onUploadImg?: UploadImgEvent; /** * 是否页面内全屏 * * @default false */ pageFullscreen?: boolean; /** * 是否展开预览 * * @default true */ preview?: boolean; /** * 是否展开html预览 * * @default false */ htmlPreview?: boolean; /** * 工具栏选择显示 * * @default allToolbar */ toolbars?: Array<ToolbarNames>; /** * 工具栏选择不显示 * * @default [] */ toolbarsExclude?: Array<ToolbarNames>; /** * 格式化md * * @default true */ noPrettier?: boolean; /** * 一个tab等于空格数 * * @default 2 */ tabWidth?: number; /** * 表格预设格子数 * * @default [6, 4] */ tableShape?: TableShapeType; /** * 空提示 * * @default '' */ placeholder?: string; /** * 自定义的工具栏列表 */ defToolbars?: Array<ReactElement>; /** * 内部错误捕获 */ onError?: ErrorEvent; /** * 页脚列表显示顺序 */ footers?: Array<Footers>; /** * 是否默认激活输入框和预览框同步滚动 * * @default true */ scrollAuto?: boolean; /** * 自定义的也叫工具组件列表 */ defFooters?: Array<string | ReactElement>; noUploadImg?: boolean; /** * 文本区域自动获得焦点 * * @default false */ autoFocus?: boolean; /** * 禁用文本区域 * * @default false */ disabled?: boolean; /** * 文本区域为只读 * * @default false */ readOnly?: boolean; /** * 文本区域允许的最大字符数 */ maxLength?: number; /** * 是否启用自动识别粘贴代码类别 * 目前支持 vscode 复制的代码识别 * * @default false */ autoDetectCode?: boolean; /** * 输入框失去焦点时触发事件 */ onBlur?: (event: FocusEvent) => void; /** * 输入框获得焦点时触发事件 */ onFocus?: (event: FocusEvent) => void; /** * @codemirror/autocomplete匹配关键词的方法列表 * * 它会被像下面这样嵌入编辑器 * * import { autocompletion } from '@codemirror/autocomplete'; * autocompletion({ * override: [...completions] * }) */ completions?: Array<CompletionSource>; /** * 是否在工具栏下面显示对应的文字名称 * * @default false */ showToolbarName?: boolean; /** * 字符输入事件 */ onInput?: (e: Event) => void; /** * 拖放事件 * * @param event */ onDrop?: (event: DragEvent) => void; /** * 输入框的默认宽度 * * @example '100px'/'50%' */ inputBoxWidth?: string; /** * 输入框宽度变化事件 */ onInputBoxWidthChange?: (width: string) => void; /** * 替换粘贴的图片链接 * * @param t 图片链接 * @returns */ transformImgUrl?: (t: string) => string | Promise<string>; /** * 内置的目录显示的状态 * * 'fixed': 悬浮在内容上方 * 'flat': 展示在右侧 * * \>=5.3.0 * * @default 'fixed' */ catalogLayout?: 'fixed' | 'flat'; /** * 控制最大显示的目录层级 * * >= v5.5.0 */ catalogMaxDepth?: number; /** * 浮动工具栏 * * @version 6.0.0 * @default [] */ floatingToolbars?: Array<ToolbarNames>; } export interface ContextType { editorId: string; tabWidth: number; highlight: { js: Partial<HTMLElementTagNameMap['script']>; css: Partial<HTMLElementTagNameMap['link']>; }; showCodeRowNumber: boolean; usedLanguageText: StaticTextDefaultValue; theme: Themes; language: string; previewTheme: PreviewThemes; customIcon: CustomIcon; rootRef: RefObject<HTMLDivElement | null> | null; disabled: boolean | undefined; showToolbarName?: boolean; setting: SettingType; updateSetting: UpdateSetting; tableShape: TableShapeType; catalogVisible: boolean; noUploadImg: boolean; noPrettier: boolean; codeTheme: string; defToolbars: Array<ReactElement>; floatingToolbars: Array<ToolbarNames>; } export interface MermaidTemplate { /** * 流程图 */ flow?: string; /** * 时序图 */ sequence?: string; /** * 甘特图 */ gantt?: string; /** * 类图 */ class?: string; /** * 状态图 */ state?: string; /** * 饼图 */ pie?: string; /** * 关系图 */ relationship?: string; /** * 旅程图 */ journey?: string; } export interface MarkdownItConfigPlugin { type: string; plugin: markdownit.PluginWithParams; options: any; } /** * CodeMirror扩展类型 * * ^6.0.0 */ export interface CodeMirrorExtension { /** * 仅用来提供开发者分别不同扩展的依据 */ type: string; /** * CodeMirror的扩展 */ extension: Extension | ((options: any) => Extension); /** * 包裹扩展的Compartment,只有部分扩展有,提供扩展更新的能力 */ compartment?: Compartment; options?: any; } export interface GlobalConfig { /** * 编辑器内部依赖库 */ editorExtensions: { highlight?: { instance?: any; js?: string; css?: CodeCss; }; prettier?: { prettierInstance?: any; parserMarkdownInstance?: any; standaloneJs?: string; parserMarkdownJs?: string; }; cropper?: { instance?: any; js?: string; css?: string; }; screenfull?: { instance?: any; js?: string; }; mermaid?: { instance?: any; js?: string; /** * 是否启用缩放功能 * * @default true */ enableZoom?: boolean; }; katex?: { instance?: any; js?: string; css?: string; }; echarts?: { instance?: any; js?: string; }; }; /** * 对应editorExtensions中的cdn链接标签属性 * * 不要尝试在editorExtensionsAttrs定义script的src\onload\id,link的rel\href\id * 它们会被默认值覆盖 */ editorExtensionsAttrs: { highlight?: { js?: Partial<HTMLElementTagNameMap['script']>; css?: CodeCssAttrs; }; prettier?: { standaloneJs?: Partial<HTMLElementTagNameMap['script']>; parserMarkdownJs?: Partial<HTMLElementTagNameMap['script']>; }; cropper?: { js?: Partial<HTMLElementTagNameMap['script']>; css?: Partial<HTMLElementTagNameMap['link']>; }; screenfull?: { js?: Partial<HTMLElementTagNameMap['script']>; }; mermaid?: { js?: Partial<HTMLElementTagNameMap['script']>; }; katex?: { js?: Partial<HTMLElementTagNameMap['script']>; css?: Partial<HTMLElementTagNameMap['link']>; }; echarts?: { js?: Partial<HTMLElementTagNameMap['script']>; }; }; editorConfig: { /** * 自定义提示语言 */ languageUserDefined?: { [key: string]: StaticTextDefaultValue; }; /** * 自定义内部mermaid模块 */ mermaidTemplate?: MermaidTemplate; /** * 输入渲染延迟(ms) */ renderDelay?: number; /** * 内部的弹窗、下拉框等内联zIndex * @default 20000 */ zIndex?: number; }; /** * 根据主题和内部默认的codeMirror扩展自定义新的扩展 * * @params theme 当前主题 * @params innerExtensions 当前主题下的扩展列表 * [keymap, minimalSetup, markdown, EditorView.lineWrapping, EditorView.updateListener, EditorView.domEventHandlers, oneDark??oneLight] * [快捷键, 最低配置, markdown识别, 横向自动换行, 更新事件, dom监听事件, oneDark主题(暗夜模式下), oneLight(默认模式下)] * * @params keyBindings md-editor-v3内置的快捷键 */ codeMirrorExtensions: (extensions: Array<CodeMirrorExtension>, options: { editorId: string; theme: Themes; keyBindings: Array<KeyBinding>; }) => Array<CodeMirrorExtension>; /** * 自定义markdown-it核心库扩展、属性等 */ markdownItConfig: (md: markdownit, options: { editorId: string; }) => void; /** * 挑选编辑器已预设的markdownIt的扩展 * * @param plugins markdownIt的扩展,带编辑器已设定的属性 * @returns plugins */ markdownItPlugins: (plugins: Array<MarkdownItConfigPlugin>, options: { editorId: string; }) => Array<MarkdownItConfigPlugin>; /** * mermaid配置项 * * @param base * @returns */ mermaidConfig: (base: any) => any; /** * katex配置 * * @param baseConfig * @returns */ katexConfig: (baseConfig: any) => any; /** * echarts配置 * * @returns */ echartsConfig: (base: any) => any; } /** * 扩展编辑器内部功能,包括marked和一些内部依赖实例,如highlight、cropper等 */ export type Config = (options: Partial<GlobalConfig>) => void; /** * 编辑器操作潜在的错误 */ export interface InnerError { name: 'Cropper' | 'fullscreen' | 'prettier' | 'overlength' | 'mermaid'; message: string; data?: any; error?: Error; } export interface CodeCss { [key: string]: { light: string; dark: string; }; } export interface CodeCssAttrs { [key: string]: { light: Partial<HTMLElementTagNameMap['link']>; dark: Partial<HTMLElementTagNameMap['link']>; }; } export interface MdPreviewStaticProps { editorId: string; noMermaid: boolean; noKatex: boolean; noHighlight: boolean; } export interface StaticProps extends MdPreviewStaticProps { noPrettier: boolean; noUploadImg: boolean; } export type UpdateSetting = (k: keyof SettingType, v?: boolean) => void; export type ChangeEvent = (v: string) => void; export type SaveEvent = (v: string, h: Promise<string>) => void; export type UploadImgCallBackParam = string[] | Array<{ url: string; alt: string; title: string; }>; export type UploadImgCallBack = (urls: UploadImgCallBackParam) => void; export type UploadImgEvent = (files: Array<File>, callBack: UploadImgCallBack) => void; export type HtmlChangedEvent = (h: string) => void; export type GetCatalogEvent = (list: HeadList[]) => void; export type ErrorEvent = (err: InnerError) => void; export interface ExposeEvent { pageFullscreen(status: boolean): void; fullscreen(status: boolean): void; preview(status: boolean): void; previewOnly(status: boolean): void; htmlPreview(status: boolean): void; catalog(status: boolean): void; } export type DOMEventHandlers = { [e in keyof HTMLElementEventMap]?: (event: HTMLElementEventMap[e], view: EditorView) => boolean | void; }; export interface InsertParam { targetValue: string; select?: boolean; deviationStart?: number; deviationEnd?: number; } /** * 插入的内容的构造函数 */ export type InsertContentGenerator = (selectedText: string) => InsertParam; /** * 插入内容的通用函数类型 */ export type Insert = (generate: InsertContentGenerator) => void; export type FocusOption = 'start' | 'end' | { rangeAnchor?: number; rangeHead?: number; cursorPos: number; }; export interface ExposeParam { /** * 添加事件监听 * * @param eventName 事件名称 * @param callBack 事件回调函数 */ on<E extends keyof ExposeEvent, C extends ExposeEvent[E]>(eventName: E, callBack: C): void; /** * 切换页面内全屏 * * @param status 是否页面全屏 */ togglePageFullscreen(status?: boolean): void; /** * 切换屏幕全屏 * * @param status 是否屏幕全屏 */ toggleFullscreen(status?: boolean): void; /** * 切换是否显示预览 * * @param status 是否显示预览 */ togglePreview(status?: boolean): void; togglePreviewOnly(status?: boolean): void; /** * 切换是否显示html预览 * * @param status html预览状态 */ toggleHtmlPreview(status?: boolean): void; /** * 切换是否显示目录 * * @param status 是否显示目录,不设置默认相反 */ toggleCatalog(status?: boolean): void; /** * 触发保存 */ triggerSave(): void; /** * 手动向文本框插入内容 * * @param {Function} generate 构造插入内容方法 * 构造方法提供「当前选中」的内容为入参 * 返回「待插入内容」和插入的属性 * 入参 selectedText 当前选中的内容 * * targetValue 待插入内容 * select 插入后是否自动选中内容 * deviationStart 插入后选中位置的开始偏移量 * deviationEnd 插入后选中位置的结束偏移量 * */ insert: Insert; /** * 手动聚焦 * * @param options 聚焦时光标的位置,不提供默认上次失焦时的位置 */ focus(options?: FocusOption): void; /** * 手动重新渲染 */ rerender(): void; /** * 获取当前选中的文本 */ getSelectedText(): string | undefined; /** * 重置已经存在的历史记录 */ resetHistory(): void; /** * codemirror事件 * * @param handlers */ domEventHandlers(handlers: DOMEventHandlers): void; /** * 执行内部插入命令 * * @param direct */ execCommand(direct: ToolDirective): void; /** * 获取编辑器实例 */ getEditorView(): EditorView | undefined; } export type ExposePreviewParam = Pick<ExposeParam, 'rerender'>; export type CustomStrIcon = { copy?: string; 'collapse-tips'?: string; pin?: string; 'pin-off'?: string; check?: string; }; /** * 自定义图标的数据类型 */ export type CustomIcon = { [key in IconName]?: { component: any; props?: { [key: string | number | symbol]: any; }; }; } & CustomStrIcon;