前言

markdown-it是一个用来解析markdown的库,它可以将markdown编译为html,然后解析时markdown-it会根据规则生成tokens,如果需要自定义,就通过rules函数对token进行处理
我现在基于markdown-it已完成第一版编辑器,现有以下功能:

  1. 快捷编辑按钮
  2. 代码块主题切换
  3. 同步滚动
  4. 目录列表生成
  5. 内容状态缓存

预览

目前实现效果如下

预览地址:https://lhrun.github.io/md-editor/
repo:https://github.com/LHRUN/md-editor 欢迎star⭐️

编辑器设计

  1. 页面布局分四部分,顶部是快捷工具栏,然后主体内容分三部分,编辑区域(textarea)、html展示区域、目录列表(可展示隐藏),因为我是用react开发的,所以html字符串我是通过dangerouslySetInnerHTML设置
  2. markdown-it初始化
export const MD = new MarkdownIt({html: true, // 在源码中启用HTML标签linkify: true, // 将类似URL的文本自动转换为链接breaks: true, // 转换段落里的 '\n' 到 <br>highlight: function (str, lang) {return highlightFormatCode(str, lang)}
}).use(MarkdownItSub).use(MarkdownItSup).use(MarkdownItMark).use(MarkdownItDeflist).use(MarkdownItTaskLists).use(markdownItAbbr).use(markdownItFootnote)// 其余的markdownIt插件...const highlightFormatCode = (str: string, lang: string): string => {if (lang && hljs.getLanguage(lang)) {try {return codeBlockStyle(hljs.highlight(lang, str, true).value)} catch (e) {console.error(e)}}return codeBlockStyle(MD.utils.escapeHtml(str))
}const codeBlockStyle = (val: string): string => {return `<pre class="hljs" style="padding: 10px;border-radius: 10px;"><code>${val}</code></pre>`
}

快捷编辑按钮

快捷便捷按钮主要是通过判断textarea的光标位置,然后通过光标位置改变编辑器文本内容,比如添加图片

// 获取光标位置
export const getCursorPosition = (editor: HTMLTextAreaElement) => {const { selectionStart, selectionEnd } = editorreturn [selectionStart, selectionEnd]
}export const addImage = (editor: HTMLTextAreaElement,source: string,setSource: (v: string) => void
) => {const [start, end] = getCursorPosition(editor)let val = sourceif (start === end) {val = `${source.slice(0, start)}\n![图片描述](url)\n${source.slice(end)}`} else {val = `${source.slice(0, start)}\n![${source.slice(start,end)}](url)\n${source.slice(end)}`}setSource(val)
}

代码块主题切换

  • 代码块高亮我是采用了highlight.js,因为这个库提供了很多主题样式,所以主题切换,我只需要改变css link即可
// codeTheme就是已选的主题名字
useEffect(() => {if (codeTheme) {switchLink('code-style',`https://cdn.bootcdn.net/ajax/libs/highlight.js/11.6.0/styles/${codeTheme}.min.css`)}
}, [codeTheme])/*** 切换html css link* @param key link key 指定唯一标识,用于切换link* @param href link href*/
export const switchLink = (key: string, href: string) => {const head = document.headconst oldLink = head.getElementsByClassName(key)if (oldLink.length) head.removeChild(oldLink[0])const newLink = document.createElement('link')newLink.setAttribute('rel', 'stylesheet')newLink.setAttribute('type', 'text/css')newLink.setAttribute('class', key)newLink.setAttribute('href', href)newLink.onerror = (e) => {console.error(e)message.error('获取css link失败')}head.appendChild(newLink)
}

同步滚动

同步滚动是我认为最难搞的一个功能,因为我不想仅仅通过百分比来计算滚动距离,因为这样的话如果编辑区域添加了一堆图片,预览就会有非常大的高度差。 我在网上找了许多方案,最后发现markdown-it的官方实现是我能找到并能实现的最佳方案,大致实现思路是如下

  1. 首先在编译时对标题元素和段落元素添加行号
/*** 注入行号*/
const injectLineNumbers: Renderer.RenderRule = (tokens,idx,options,_env,slf
) => {let lineif (tokens[idx].map && tokens[idx].level === 0) {line = (tokens[idx].map as [number, number])[0]tokens[idx].attrJoin('class', 'line')tokens[idx].attrSet('data-line', String(line))}return slf.renderToken(tokens, idx, options)
}MD.renderer.rules.heading_open = MD.renderer.rules.paragraph_open = injectLineNumbers
  1. 滚动前计算出当前编辑区域每行对应的预览偏移距离,有标记行号的元素直接计算offset,未标记行号的元素就等比计算
/*** 获取编辑区域每行对应的预览偏移距离* @param editor 编辑元素* @param review 预览元素* @returns number[]*/
const buildScrollMap = (editor: HTMLTextAreaElement,review: HTMLDivElement
) => {const lineHeightMap: number[] = []let linesCount = 0 // 编辑区总行数/*** 临时创建元素获取每次换行之间的总行数*/const sourceLine = document.createElement('div')sourceLine.style.position = 'absolute'sourceLine.style.visibility = 'hidden'sourceLine.style.height = 'auto'sourceLine.style.width = `${editor.clientWidth}px`sourceLine.style.fontSize = '15px'sourceLine.style.lineHeight = `${LINE_HEIGHT}px`document.body.appendChild(sourceLine)let acc = 0editor.value.split('\n').forEach((str) => {lineHeightMap.push(acc)if (str.length === 0) {acc++return}sourceLine.textContent = strconst h = sourceLine.offsetHeightacc += Math.round(h / LINE_HEIGHT)})sourceLine.remove()lineHeightMap.push(acc)linesCount = acc// 最终输出的偏移mapconst _scrollMap: number[] = new Array(linesCount).fill(-1)/*** 获取标记行号的offset距离*/const nonEmptyList = []nonEmptyList.push(0)_scrollMap[0] = 0document.querySelectorAll('.line').forEach((el) => {let t: string | number = el.getAttribute('data-line') as stringif (t === '') {return}t = lineHeightMap[Number(t)]if (t !== 0) {nonEmptyList.push(t)}_scrollMap[t] = Math.round((el as HTMLElement).offsetTop - review.offsetTop)})nonEmptyList.push(linesCount)_scrollMap[linesCount] = review.scrollHeight/*** 未标记行号的元素等比计算*/let pos = 0for (let i = 1; i < linesCount; i++) {if (_scrollMap[i] !== -1) {pos++continue}const a = nonEmptyList[pos]const b = nonEmptyList[pos + 1]_scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a))}return _scrollMap
}
  1. 编辑区域滚动根据具体行获取需滚动高度
export const editorScroll = (editor: HTMLTextAreaElement,preview: HTMLDivElement
) => {if (!scrollMap) {scrollMap = buildScrollMap(editor, preview)}const lineNo = Math.floor(editor.scrollTop / LINE_HEIGHT)const posTo = scrollMap[lineNo]preview.scrollTo({ top: posTo })
}
  1. 预览区域滚动根据当前的滚动高度查对应编辑区域的行,然后根据计算滚动高度
export const previewScroll = (editor: HTMLTextAreaElement,preview: HTMLDivElement
) => {if (!scrollMap) {scrollMap = buildScrollMap(editor, preview)}const lines = Object.keys(scrollMap)if (lines.length < 1) {return}let line = lines[0]for (let i = 1; i < lines.length; i++) {if (scrollMap[Number(lines[i])] < preview.scrollTop) {line = lines[i]continue}break}editor.scrollTo({ top: LINE_HEIGHT * Number(line) })
}

同步滚动注意点

  1. 在改变编辑内容和窗口大小时需清空计算结果,因为这两个一改变,每行的偏移距离就会发生变化,在滚动时需要重新计算
  2. 同步滚动时会有一个无限触发的问题,因为编辑区域滚动,会触发预览区域的scrollTo(),然后预览区域的滚动监听方法就会被触发,然后这样就会无限触发下去,所以需要一个变量记住当前的手动滚动的区域,进行限制

目录列表生成

目录列表通过rules的heading_open方法,获取当前标题的token,然后通过token得出标题的具体内容进行拼接,最后根据level计算字体大小

  • 获取标题内容
const getTitle = (tokens: Token[], idx: number) => {const { children } = tokens[idx + 1]const { markup } = tokens[idx]const val = children?.reduce((acc, cur) => `${acc}${cur.content}`, '') || ''toc.push({val,level: markup.length})
}
  • html展示
{showToc && (<div className={styles.toc}><div className={styles.tocTitle}>目录</div><div>{tocList.map(({ val, level }, index) => {const fontSize = ((7 - level) / 10) * 40return (<divstyle={{marginLeft: `${level * 10}px`,fontSize: `${fontSize > 12 ? fontSize : 12}px`}}key={index}>{val}</div>)})}</div></div>
)}

总结

可能完成的有点粗糙,以后有时间继续完善细节,有问题欢迎讨论

基于markdown-it打造的markdown编辑器相关推荐

  1. 马克飞象怎么转成html,专为印象笔记打造的 Markdown 编辑器:马克飞象

    原标题:专为印象笔记打造的 Markdown 编辑器:马克飞象 近期,少数派跟大家分享了 Markdown 工具合辑<想试试其它写作工具?11 款好用的 Markdown 编辑器推荐>.关 ...

  2. 使用Typora+PicGo+Gitee打造全新Markdown博客创作环境

    使用Typora+PicGo+Gitee打造全新Markdown博客创作环境 文章目录 第一部分:概述 1.1 问题来源 1.2 软件简介 第二部分:实现 2.1 软件下载 2.2 GiTee账户令牌 ...

  3. javascript官方文档_基于Javascript编写的开源Markdown和HTML相互转换器——showdown

    介绍 showdown是一个基于Javascript编写的开源Markdown和HTML相互转换器,showdown可以用在客户端(浏览器)或者服务端(nodejs).shodown还支持原始规范中未 ...

  4. MarkDown语法详解(Typora编辑器)

    MarkDown语法详解(Typora编辑器) 即使再小的帆也能远航~ 目录 Mrakdown简介 Markdown标题 Markdown字体 各种线 引用 图片 超链接 列表 表格 代码 锚(mao ...

  5. 《不会吧?不会吧?不会还有人不知道 Markdown 吧?|CSDN编辑器测评》

    文章目录 1. CSDN 的 Markdown 是最好的么? 1.1 CSDN Markdown 编辑器的优点 1.2 CSDN Markdown 编辑器的缺点 2. 使用 Markdown 的小技巧 ...

  6. md是什么类型的文件?怎么打开md文件,Markdown的编写,Markdown转化为html

    md 就是 Markdown 的文件,Markdown 是一种轻量级标记语言.CSDN 的博客就是用 markdown 来编写的呢!html 大家不陌生吧,他是超文本标记语言,他们都是标记语言,那有什 ...

  7. markdown如何调整行距_反Markdown试验:用Markdown的思维来使用Word

    注意:这只是一个试验,不一定能用于实际书写!!! Markdown最吸引人的是书写时那种专注内容.流畅的书写体验.但是Markdown又有其不尽人意的地方,那么能不能使用其他编辑器达到类似的书写体验呢 ...

  8. 【Markdown基础教程】Markdown介绍

    本文主要讲解关于Markdown的概念 1.Markdown是什么? Markdown是什么?有什么特点?,这就是我最开始看到这个单词的想法. 于是,总结出来这几条 1.Markdown是一种轻量级标 ...

  9. 什么是Markdown?为什么需要使用Markdown?

    本文的面向对向是具备一定程度的电脑操作水平的人,理解文档格式.编程语言,而非电脑小白,因为Markdown不是适合所有人的,它需要一定的技术,如果没有技术也乐意折腾Markdown,则至少需要接受新事 ...

  10. Markdown转PDF→利用 Markdown 制作电子书|非常简单

    Markdown转PDF→利用 Markdown 制作电子书 前言 遇到什么问题就解决什么问题,什么不会就学什么. 是这样的,因为最近在看数据分析的书籍<利用Python进行数据分析 原书第2版 ...

最新文章

  1. “AI让老百姓最多跑一次”:智源科学家打造下一代政务服务智能助手
  2. Redis入门(暂不更新)
  3. Spring核心系列之Spring中的事务
  4. mysql导入数据load data infile用法整理
  5. 【前端面试题】2021/3/12挺经典的面试题,这个经历很深刻。
  6. fastai学习:05_pet_breeds Questionnaire
  7. iOS SDK 介绍及导入
  8. centos7下发邮件给自己的QQ邮箱
  9. python一般用什么平台好_python哪几个平台好用
  10. 实用的C盘清理技巧(亲测有效)
  11. 达内微软mta证书有用吗_mta证书值得拿吗 怎么样才能拿到mta证书
  12. 一梦江湖(楚留香)自用日常手机脚本
  13. 未来已来——工作空间 WorkSpace 和物联网 IoT (2)
  14. 计算机组装过程注意事项,DIY小白必看 新手组装电脑常犯的四个注意事项
  15. [USACO 2012 January Gold] Video Game Combos
  16. Attempting to capture an EagerTensor without building a function
  17. Oracle gsd服务是什么,如何处理11gR2 RAC下oc4j和gsd服务为OFFLINE状态
  18. 10个开源/免费的电子商务平台
  19. MoveIt! 学习笔记14 - Kinematics Configuration/Fake Controller Manager
  20. ROS实现串口解析GPS协议,并发布到话题

热门文章

  1. 硅谷女性成功的五大秘诀
  2. 盘点7款应用最广泛的 Linux 桌面环境
  3. deepstream 2D 3D 动作识别(包括file、rtsp输入输出)
  4. Revit Architecture基础概述学习记录
  5. 索尼信息系统软件测试,〖分享〗索尼工程模式*#*#7378423*#*#,各项测试!
  6. Odoo与浪潮合资研发PS Cloud之基于评分的线索分配
  7. 邓俊辉数据结构学习笔记1
  8. 蓝牙智能音箱技术方案开发
  9. 技术总结 | 三种常见单相整流电路
  10. matlab安装的根目录查看