全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2022-11-24_如何用 ProseMirror 解析和渲染 markdown

您的位置:首页 >> 新闻 >> 行业资讯

如何用 ProseMirror 解析和渲染 markdown 前言上一篇我们简单搭建了一个 electron 应用的开发环境,启动后,窗口加载了一个网页。现在我们要加入我们的主体功能 —— Markdown。在这里,我借助 ProseMirror 来开发编辑器。我们先简单学习一下如何用 ProseMirror 搭建一个富文本编辑器。 ProseMirror简易编辑器ProseMirror 有几个必要的模块: prosemirror-model定义了编辑器的Document Model, 它用来描述编辑器的内容.prosemirror-state提供了一个描述编辑器完整状态的单一数据结构, 包括编辑器的选区操作, 和一个用来处理从当前 state 到下一个 state 的一个叫做transaction的系统.prosemirror-view用来将给定的 state 展示成相对应的可编辑元素显示在编辑器中, 同时处理用户交互.prosemirror-transform包含了一种可以被重做和撤销的修改文档的功能, 它是prosemirror-state库transaction功能的基础, 这使得撤销操作历史记录和协同编辑成为可能.其他还有一些模块,如prosemirror-collab、prosemirror-gapcursor等,使用频率也很高,具体看需求。 现在我们简单地使用上面几个模块来构建一个编辑器。 import { EditorState } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { Schema, DOMParser } from "prosemirror-model"; import { schema } from "prosemirror-schema-basic"; import { useEffect, useRef } from "react"; import React from "react"; export const Editor = () = { const schemaIntance = new Schema({ nodes: schema.spec.nodes, marks: schema.spec.marks, }); const view = useRefEditorView useEffect(() = { view.current = new EditorView(document.getElementById("editor"), { state: EditorState.create({ doc: DOMParser.fromSchema(schemaIntance).parse( document.getElementById("content")! ), schema: schemaIntance }), }); }, []); return ( div div id="content" style={{ height: 0, overflow: "hidden" }} Hello ProseMirror /div div id="editor"/div /div ); }; image.png这里的schema我就直接用的prosemirror-schema-basic提供,支持的Node和Mark不多。 上面这个编辑器还很简陋,像输入回车键换行等常见的操作都还没有。我们可以借助 ProseMirror 的其他模块继续增强编辑器的能力。官方已经提供了一些常用的模块: prosemirror-history该模块实现了 redo/undo 操作。 prosemirror-keymap该模块可以将键名映射到 command 类型的函数。 prosemirror-commands这个包提供了很多基本的编辑 commands, 包括在编辑器中按照你的期望映射 enter 和 delete 按键的行为。 import{undo,redo,history}from"prosemirror-history"; import{keymap}from"prosemirror-keymap"; import{baseKeymap}from"prosemirror-commands"; //... useEffect(()={ view.current=newEditorView(document.getElementById("editor"),{ state:EditorState.create({ doc:DOMParser.fromSchema(schemaIntance).parse( document.getElementById("content")! ), schema:schemaIntance, plugins:[ history(), keymap({"Mod-z":undo,"Mod-y":redo}), keymap(baseKeymap), ], }), }, 如图所示,我加入了keymap和history,现在这个编辑器支持redoundo了。 Pluginsprosemirror-stateprosemirror-state提供了编写插件的能力,上面用的keymap和history都是Plugin。 import{Plugin}from"prosemirror-state" functionkeydownHandler(bindings){ returnfunction(view,event){ //... } } constkeymap=function(bindings){ returnnewPlugin({ props:{ handleKeyDown:keydownHandler(bindings) } }) } keymap插件去除其他复杂的逻辑,核心的部分就是上图所示。 prosemirror-inputrulesprosemirror-inputrules这个模块可以配置匹配规则,当我们输入的文本符合条件时,可以替换成别的文本或者节点, 或者用transaction做更复杂的事情。它内置一些inputrule,比如emDash, 连续输入两个-会替换成一个——, 我们想要实现的Markdown是像Typora的那种,这里需要借助prosemirror-inputrules。我们以输入 **hello** 自动变成hello为例 import{inputRules,InputRule}from"prosemirror-inputrules"; //... view.current=newEditorView(document.getElementById("editor"),{ state:EditorState.create({ doc:DOMParser.fromSchema(schemaIntance).parse( document.getElementById("content")! ), schema:schemaIntance, plugins:[ inputRules({ rules:[ newInputRule( /\*\*[^\*]{1,}\*\*$/, (state:EditorState,match,from,to,text)={ constmark=state.schema.mark("strong"); conststr=match[0].substring(2,match[0].length-2); constnode=state.schema.text(str,[mark]); returnstate.tr.replaceWith(from,to,node); } ), ], }), ], }), md-strong.gifprosemirror-inputrules还提供了一个 command ——undoInputRule, 它可以将我们刚刚输入的hello重新变成 **hello**,我们结合history和keymap来使用。 import{undo,redo,history}from"prosemirror-history"; import{keymap}from"prosemirror-keymap"; import{baseKeymap,chainCommands}from"prosemirror-commands"; import{ inputRules, InputRule, undoInputRule, }from"prosemirror-inputrules"; //... view.current=newEditorView(document.getElementById("editor"),{ state:EditorState.create({ doc:DOMParser.fromSchema(schemaIntance).parse( document.getElementById("content")! ), schema:schemaIntance, plugins:[ history(), keymap({ "Mod-z":chainCommands(undoInputRule,undo), "Mod-y":redo, }), keymap(baseKeymap), inputRules({ rules:[ newInputRule( /\*\*[^\*]{1,}\*\*$/, (state:EditorState,match,from,to,text)={ constmark=state.schema.mark("strong"); conststr=match[0].substring(2,match[0].length-2); constnode=state.schema.text(str,[mark]); returnstate.tr.replaceWith(from,to,node); } ), ], }), ], }), undo-inputrule.gif可以看到,按键CommandOrCtrl+z可以回到之前的状态,再重新修改。 Remirror前面,我们都是用ProseMirror来构建编辑器的。现在,我要给大家介绍一个很好用的 react 库 ——Remirror。 Remirror是基于ProseMirror的用于构建跨平台富文本编辑器的 React 工具包,它可以和 react 很好的配合工作。Remirror 同样不是像 Draft.js 那样是一个开箱即用的方案。 Installnpmaddremirror@remirror/react@remirror/pm Create Manager开始初始化一个 remirror 编辑器之前,我们要先创建一个 manager,它用来控制编辑器的行为。 manager 只提供了最基础基础的功能,需要配合 extensions 才能发挥作用。这里我使用了 MarkdownExtension 扩展,它给我们提供了简单的解析 markdown 的功能。 import React from "react"; import { MarkdownExtension, BoldExtension } from "remirror/extensions"; import { Remirror, useRemirror } from "@remirror/react"; import "remirror/styles/all.css"; const Editor = () = { const { manager, state } = useRemirror({ extensions: () = [new MarkdownExtension()], content: "", selection: "start", stringHandler: "markdown" }); return ( div className="remirror-container" Remirror manager={manager} initialContent={state} / /div ); }; export default function App() { return Editor / } content 用于提供初始化的内容,你可以试试 content: "a"。codesandbox 如图所示,remirror/extensions 这个模块中还提供了很多内置的扩展,大家可以根据需要直接使用。 image.png添加扩展我们先添加一个 BoldExtension 看效果。codesandbox import React from "react"; import { MarkdownExtension, BoldExtension } from "remirror/extensions"; import { Remirror, useRemirror } from "@remirror/react"; import "remirror/styles/all.css"; const Editor = () = { const { manager, state } = useRemirror({ extensions: () = [new MarkdownExtension(), new BoldExtension()], content: "", selection: "start", stringHandler: "markdown" }); return ( div className="remirror-container" Remirror manager={manager} initialContent={state} / /div ); }; export default function App() { return Editor / } 尝试输入 **bold**,可以看到编辑器自动将它转成了bold 自定义扩展@remirror 的扩展写起来是很方便的,我们简单来看一下 @remirror/extension-bold 的具体实现。 remirror 的扩展用到了很多 @remirror/core 提供的装饰器。 exportinterfaceBoldOptions{ weight?:StaticFontWeightProperty } @extensionBoldOptions({ defaultOptions:{weight:undefined}, staticKeys:['weight'], }) exportclassBoldExtensionextendsMarkExtensionBoldOptions{ getname(){ return'bold'asconst; } createTags(){ return[ExtensionTag.FormattingMark,ExtensionTag.FontStyle]; } createMarkSpec(extra:ApplySchemaAttributes,override:MarkSpecOverride):MarkExtensionSpec{ return{ ...override, attrs:extra.defaults(), parseDOM:[ { tag:'strong', getAttrs:extra.parse, }, //ThisworksaroundaGoogleDocsmisbehaviorwhere //pastedcontentwillbeinexplicablywrappedin`` //tagswithafont-weightnormal. { tag:'b', getAttrs:(node)= isElementDomNode(node)node.style.fontWeight!=='normal' ?extra.parse(node) :false, }, { style:'font-weight', getAttrs:(node)= isString(node)/^(bold(er)?|[5-9]\d{2,})$/.test(node)?null:false, }, ...(override.parseDOM??[]), ], toDOM:(node)={ const{weight}=this.options; if(weight){ return['strong',{'font-weight':weight.toString()},0]; } return['strong',extra.dom(node),0]; }, } createInputRules():InputRule[]{ return[ markInputRule({ regexp:/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, type:this.type, ignoreWhitespace:true, }), } @command(toggleBoldOptions) toggleBold(selection?:PrimitiveSelection):CommandFunction{ returntoggleMark({type:this.type,selection } @command() setBold(selection?:PrimitiveSelection):CommandFunction{ return({tr,dispatch})={ const{from,to}=getTextSelection(selection??tr.selection,tr.doc); dispatch?.(tr.addMark(from,to,this.type.create())); returntrue; } @command() removeBold(selection?:PrimitiveSelection):CommandFunction{ return({tr,dispatch})={ const{from,to}=getTextSelection(selection??tr.selection,tr.doc); if(!tr.doc.rangeHasMark(from,to,this.type)){ returnfalse; } dispatch?.(tr.removeMark(from,to,this.type)); returntrue; } @keyBinding({shortcut:NamedShortcut.Bold,command:'toggleBold'}) shortcut(props:KeyBindingProps):boolean{ returnthis.toggleBold()(props); } } 这个里面的一些写法,看起来是不是和前面我们直接用ProseMirror实现的有些类似,实际就是用的那些模块。 渲染编辑器和菜单import{ BoldExtension, HeadingExtension, ItalicExtension, MarkdownExtension, PlaceholderExtension, }from"remirror/extensions"; //... constMenu=()=buttononClick={()=alert("buttonb")}/button exportconstMarkdown=()={ const{manager,state}=useRemirror({ extensions:()=[ newPlaceholderExtension({ placeholder:"请输入文字", }), newBoldExtension(), newItalicExtension(), newMarkdownExtension(), newHeadingExtension(), ], content:"", selection:"start", stringHandler:"markdown", return( divclassName="remirror-container" Remirrormanager={manager}initialContent={state}hooks={hooks} Menu/ EditorComponent/ /Remirror /div }; 当你需要自定义渲染编辑器的菜单时,你就需要使用EditorComponent,Remirror组件不接受 children 时默认会使用该组件渲染。这里Menu菜单就简单写了个按钮,后续我们再优化。 image.png总结基本上,参考Remirror的插件写法或者直接使用Remirror就可以简单构建出一个Markdown编辑器,接下来就看大家的发挥了。 阅读原文

上一篇:2018-12-07_好物互送平台“享物说”位列电梯海报花费榜单第9位 下一篇:2018-08-08_前沿 | GAN用于材料设计:哈佛大学新研究登上Science

TAG标签:

17
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价