共计 18957 个字符,预计需要花费 48 分钟才能阅读完成。
笔者平时写文章应用的都是 Markdown
,然而公布的时候就会遇到一些平台不反对Markdown
的状况,重排是不可能重排的,所以都会应用一些 Markdown
转富文本的工具,比方 markdown-nice,用的多了就会好奇是怎么实现的,于是就有了本篇文章。
markdown-nice
是一个基于 React
构建的我的项目,先来看一下它的整体页面:
一个顶部工具栏,两头三个并列的区域,别离是编辑区域、预览区域、自定义主题区域,自定义主题区域默认是暗藏的。
大体上就是一个 Markdown
编辑器,减少了一些对各个平台的适配而已。
编辑器
编辑器应用的是 CodeMirror,具体来说是一个二次封装的组件 React-CodeMirror:
import CodeMirror from "@uiw/react-codemirror";
class App extends Component {render() {
return (
<CodeMirror
value={this.props.content.content}
options={{
theme: "md-mirror",// 主题
keyMap: "sublime",// 快捷键
mode: "markdown",// 模式,也就是语言类型
lineWrapping: true,// 开启超长换行
lineNumbers: false,// 不显示行号
extraKeys: {// 配置快捷键
...bindHotkeys(this.props.content, this.props.dialog),
Tab: betterTab,
RightClick: rightClick,
},
}}
onChange={this.handleThrottleChange}
onScroll={this.handleScroll}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
onDrop={this.handleDrop}
onPaste={this.handlePaste}
ref={this.getInstance}
/>
)
}
}
快捷键、命令
markdown-nice
通过 extraKeys
选项设置一些快捷键,此外还在工具栏中减少了一些快捷按钮:
这些快捷键或者命令按钮操作文本内容的逻辑根本是统一的,先获取以后选区的内容:
const selected = editor.getSelection()
而后进行加工批改:
`**${selected}**`
最初替换选区的内容:
editor.replaceSelection(`**${selected}**`)
此外也能够批改光标的地位来晋升体验,比方加粗操作后光标地位会在文字前面,而不是 *
前面就是因为 markdown-nice
在替换完选区内容后还批改了光标的地位:
export const bold = (editor, selection) => {editor.replaceSelection(`**${selection}**`);
const cursor = editor.getCursor();
cursor.ch -= 2;// 光标地位向前两个字符
editor.setCursor(cursor);
};
表格
Markdown
的表格语法手写起来是比拟麻烦的,markdown-nice
对于表格只提供了帮你插入表格语法符号的性能,你能够输出要插入的表格行列数:
确认当前会主动插入符号:
实现其实就是一个字符串的拼接逻辑:
const text = this.buildFormFormat(this.state.rowNum, this.state.columnNum);
buildFormFormat = (rowNum, columnNum) => {
let formFormat = "";
// 起码会创立三行
for (let i = 0; i < 3; i++) {formFormat += this.buildRow(i, columnNum);
}
// 超过三行
for (let i = 3; i <= rowNum; i++) {formFormat += this.buildRow(i, columnNum);
}
return formFormat;
};
buildRow = (rowNum, columnNum) => {
let appendText = "|";
// 第一行为表头和内容的分隔
if (rowNum === 1) {
appendText += "--- |";
for (let i = 0; i < columnNum - 1; i++) {appendText += "--- |";}
} else {
appendText += "|";
for (let i = 0; i < columnNum - 1; i++) {appendText += "|";}
}
return appendText + (/windows|win32/i.test(navigator.userAgent) ? "\r\n" : "\n");
};
表格字符生成当前替换以后选区内容即可:
handleOk = () => {const {markdownEditor} = this.props.content;
const cursor = markdownEditor.getCursor();
const text = this.buildFormFormat(this.state.rowNum, this.state.columnNum);
markdownEditor.replaceSelection(text);
cursor.ch += 2;
markdownEditor.setCursor(cursor);
markdownEditor.focus();};
同样批改了光标地位并且让编辑器从新聚焦。
图片上传
markdown-nice
反对间接拖动图片到编辑区域进行上传和粘贴图片间接上传,这是通过监听 CodeMirror
编辑器的 drop
和paste
事件实现的:
<CodeMirror
onDrop={this.handleDrop}
onPaste={this.handlePaste}
/>
handleDrop = (instance, e) => {if (!(e.dataTransfer && e.dataTransfer.files)) {return;}
for (let i = 0; i < e.dataTransfer.files.length; i++) {uploadAdaptor({file: e.dataTransfer.files[i], content: this.props.content});
}
};
handlePaste = (instance, e) => {if (e.clipboardData && e.clipboardData.files) {for (let i = 0; i < e.clipboardData.files.length; i++) {uploadAdaptor({file: e.clipboardData.files[i], content: this.props.content});
}
}
}
判断如果拖拽或粘贴的数据中存在文件那么会调用 uploadAdaptor
办法:
export const uploadAdaptor = (...args) => {const type = localStorage.getItem(IMAGE_HOSTING_TYPE);
if (type === IMAGE_HOSTING_NAMES.aliyun) {const config = JSON.parse(window.localStorage.getItem(ALIOSS_IMAGE_HOSTING));
if (
!config.region.length ||
!config.accessKeyId.length ||
!config.accessKeySecret.length ||
!config.bucket.length
) {message.error("请先配置阿里云图床");
return false;
}
return aliOSSUpload(...args);
}
}
省略了其余类型的图床,以阿里云 OSS
为例,会先检查一下相干的配置是否存在,存在的话则会调用 aliOSSUpload
办法:
import OSS from "ali-oss";
export const aliOSSUpload = ({file = {},
onSuccess = () => {},
onError = () => {},
images = [],
content = null, // store content
}) => {const config = JSON.parse(window.localStorage.getItem(ALIOSS_IMAGE_HOSTING));
// 将文件类型转成 base64 类型
const base64Reader = new FileReader();
base64Reader.readAsDataURL(file);
base64Reader.onload = (e) => {
const urlData = e.target.result;
const base64 = urlData.split(",").pop();
// 获取文件类型
const fileType = urlData
.split(";")
.shift()
.split(":")
.pop();
// base64 转 blob
const blob = toBlob(base64, fileType);
// blob 转 arrayBuffer
const bufferReader = new FileReader();
bufferReader.readAsArrayBuffer(blob);
bufferReader.onload = (event) => {const buffer = new OSS.Buffer(event.target.result);
aliOSSPutObject({config, file, buffer, onSuccess, onError, images, content});
};
};
};
这一步次要是将文件类型转换成了 arrayBuffer
类型,最初会调用 aliOSSPutObject
进行文件上传操作:
const aliOSSPutObject = ({config, file, buffer, onSuccess, onError, images, content}) => {let client = new OSS(config);
// 上传文件名拼接上以后工夫
const OSSName = getOSSName(file.name);
// 执行上传操作
client
.put(OSSName, buffer)
.then((response) => {const names = file.name.split(".");
names.pop();
const filename = names.join(".");
const image = {
filename, // 名字不变并且去掉后缀
url: response.url,
};
// 插入到文档
if (content) {writeToEditor({content, image});
}
})
.catch((error) => {console.log(error);
});
};
上传胜利后会把图片插入到文档:
function writeToEditor({content, image}) {const isContainImgName = window.localStorage.getItem(IS_CONTAIN_IMG_NAME) === "true";
let text = "";
// 是否带上文件名
if (isContainImgName) {text = `\n![${image.filename}](${image.url})\n`;
} else {text = `\n![](${image.url})\n`;
}
const {markdownEditor} = content;
// 替换以后选区
const cursor = markdownEditor.getCursor();
markdownEditor.replaceSelection(text, cursor);
content.setContent(markdownEditor.getValue());
}
其余各大平台的具体上传逻辑能够参考源码:imageHosting.js。
格式化 Markdown
markdown-nice
反对格式化 Markdown
的性能,也就是丑化性能,比方:
丑化后:
格式化应用的是 prettier:
import prettier from "prettier/standalone";
import prettierMarkdown from "prettier/parser-markdown";
export const formatDoc = (content, store) => {content = handlePrettierDoc(content);
// 给被中文包裹的 `$` 符号前后增加空格
content = content.replace(/([\u4e00-\u9fa5])\$/g, "$1 $");
content = content.replace(/\$([\u4e00-\u9fa5])/g, "$ $1");
store.setContent(content);
message.success("格式化文档实现!");
};
// 调用 prettier 进行格式化
const handlePrettierDoc = (content) => {
const prettierRes = prettier.format(content, {
parser: "markdown",
plugins: [prettierMarkdown],
});
return prettierRes;
};
预览
预览也就是将 Markdown
转换为 html
进行显示,预览区域只须要提供一个容器元素,比方 div
,而后将转换后的html
内容应用 div.innerHTML = html
形式追加进去即可。
目前将 Markdown
转换为 html
的开源库有很多,比方 markdown-it、marked、showdown,markdown-nice
应用的是markdown-it
。
外围代码:
const parseHtml = markdownParser.render(this.props.content.content);
return (
<section
dangerouslySetInnerHTML={{__html: parseHtml,}}
/>
)
markdownParser
即 markdown-it
实例:
import MarkdownIt from "markdown-it";
export const markdownParser = new MarkdownIt({
html: true,// 容许在源代码中存在 HTML 标签
highlight: (str, lang) => {// 代码高亮逻辑,前面再看},
});
插件
创立完 MarkdownIt
的实例后,接着注册了很多插件:
markdownParser
.use(markdownItSpan) // 在题目标签中增加 span
.use(markdownItTableContainer) // 在表格内部增加容器
.use(markdownItMath) // 数学公式
.use(markdownItLinkfoot) // 批改脚注
.use(markdownItTableOfContents, {transformLink: () => "",
includeLevel: [2, 3],
markerPattern: /^\[toc\]/im,
}) // TOC 仅反对二级和三级题目
.use(markdownItRuby) // 注音符号
.use(markdownItImplicitFigures, {figcaption: true}) // 图示
.use(markdownItDeflist) // 定义列表
.use(markdownItLiReplacer) // li 标签中退出 p 标签
.use(markdownItImageFlow) // 横屏挪动插件
.use(markdownItMultiquote) // 给多级援用加 class
.use(markdownItImsize);
插件的性能正文中也体现了。
markdown-it
会把输出的 Markdown
字符串转成一个个 token
,而后依据token
生成 html
字符串,比方 # 街角小林
会生成如下的 token
列表(删减局部字段):
[
{
"type": "heading_open",
"tag": "h1",
"nesting": 1,
"level": 0,
"children": null,
"content": "","markup":"#","info":"",
"block": true,
},
{
"type": "inline",
"tag": "","nesting": 0,"level": 1,"children": [
{
"type": "text",
"tag": "","nesting": 0,"level": 0,"children": null,"content":" 街角小林 ","markup":"",
"info": "","block": false,
}
],
"content": "街角小林",
"markup": "","info":"",
"block": true,
},
{
"type": "heading_close",
"tag": "h1",
"nesting": -1,
"level": 0,
"children": null,
"content": "","markup":"#","info":"",
"block": true
}
]
在 markdown-it
外部,实现各项工作的是一个个 rules
,其实就是一个个函数,解析的rules
分为三类:core
、block
、inline
。
core
蕴含 normalize
、block
、inline
、linkify
、replacements
、smartquotes
这些规定,会对咱们传入的 markdown
字符串按程序顺次执行上述规定,其中就蕴含着 block
和inlnie
类型的规定的执行过程,block
和 inline
相干规定就是用来生成一个个 token
的,顾名思义,一个负责生成块级类型的token
,比方题目、代码块、表格、我的项目列表等,一个负责在块级元素生成之后再生成内联类型的token
,比方文本、链接、图片等。
block
运行时会逐行扫描 markdown
字符串,对每一行字符串都会顺次执行所有块级 rule
函数,解析生成块级 token
,内置的block
规定有 table
、code
、fence
、blockquote
、hr
、list
、heading
、paragraph
等。
在 block
类型的规定解决完之后,可能会生成一种 type
为 inline
的 token
,这种 token
属于未齐全解析的 token
,所以还须要通过 inline
类型的 token
再解决一次,也就是对块级 token
的content
字段保留的字符进行解析生成内联 token
,内置的inline
规定有 text
、link
、image
等。
这些解析规定都执行完后会输入一个 token
数组,再通过 render
相干规定生成 html
字符串,所以一个 markdown-it
插件如果想干涉生成的 token
,那就通过更新、扩大、增加不同类型的解析rule
,如果想干涉依据token
生成的html
,那就通过更新、扩大、增加渲染rule
。
以上只是粗略的介绍,有趣味深刻理解的能够浏览 markdown-it
源码或上面两个系列的文章:
markdown-it 源码剖析 1 - 整体流程、markdown-it 系列文章
markdown-nice
应用的这么多插件,有些是社区的,有些是本人写的,接下来咱们看看其中两个比较简单的。
1.markdownItMultiquote
function makeRule() {return function addTableContainer(state) {
let count = 0;
let outerQuoteToekn;
for (var i = 0; i < state.tokens.length; i++) {
// 遍历所有 token
const curToken = state.tokens[i];
// 遇到 blockquote_open 类型的 token
if (curToken.type === "blockquote_open") {if (count === 0) {
// 最外层 blockquote 的 token
outerQuoteToekn = curToken;
}
count++;
continue;
}
if (count > 0) {
// 给最外层的加一个类名
outerQuoteToekn.attrs = [["class", "multiquote-" + count]];
count = 0;
}
}
};
}
export default (md) => {
// 在外围规定下减少一个自定义规定
md.core.ruler.push("blockquote-class", makeRule(md));
};
这个插件很简略,就是当存在多层嵌套的 blockquote
时给最外层的 blockquote token
增加一个类名,成果如下:
2.markdownItLiReplacer
function makeRule(md) {return function replaceListItem() {
// 笼罩了两个渲染规定
md.renderer.rules.list_item_open = function replaceOpen() {return "<li><section>";};
md.renderer.rules.list_item_close = function replaceClose() {return "</section></li>";};
};
}
export default (md) => {md.core.ruler.push("replace-li", makeRule(md));
};
这个插件就更简略了,笼罩了内置的 list_item
规定,成果就是在 li
标签内加了个 section
标签。
外链转脚注
咱们都晓得公众号最大的限度就是超链接只容许白名单内的,其余的都会被过滤掉,所以如果不做任何解决,咱们的超链接就没了,解决办法个别都是转成脚注,显示在文章开端,markdown-nice
实现这个的逻辑比较复杂,会先更改 Markdown
内容,将:
[现实青年实验室](http://lxqnsys.com/)
格式化为:
[现实青年实验室](http://lxqnsys.com/ "现实青年实验室")
也就是将题目补上了,而后再通过 markdown-it
插件解决token
,生成脚注:
markdownParser
.use(markdownItLinkfoot) // 批改脚注
这个插件的实现也比较复杂,有趣味的能够浏览源码:markdown-it-linkfoot.js。
其实咱们能够抉择另一种比较简单的思路,咱们能够笼罩掉 markdown-it
外部的链接 token
渲染规定,同时收集所有的链接数据,最初咱们本人来生成 html
字符串拼接到 markdown-it
输入的 html
字符串上。
比方咱们创立一个 markdownItLinkfoot2
插件,注册:
// 用来收集所有的链接
export const linkList = []
markdownParser
.use(markdownItLinkfoot2, linkList)
把收集链接的数组通过选项传给插件,接下来是插件的代码:
function makeRule(md, linkList) {return function() {
// 每次从新解析前都清空数组和计数器
linkList.splice(0, linkList.length)
let index = 0
let isWeChatLink = false
// 笼罩 a 标签的开标签 token 渲染规定
md.renderer.rules.link_open = function(tokens, idx) {
// 获取以后 token
let token = tokens[idx]
// 获取链接的 url
let href = token.attrs[0] ? token.attrs[0][1] : ''
// 如果是微信域名则不须要转换
if (/^https:\/\/mp.weixin.qq.com\//.test(href)) {
isWeChatLink = true
return `<a href="${href}">`
}
// 前面跟着的是链接内的其余 token,咱们能够遍历查找文本类型的 token 作为链接题目
token = tokens[++idx]
let title = ''while(token.type !=='link_close') {if (token.type === 'text') {
title = token.content
break
}
token = tokens[++idx]
}
// 将链接增加到数组里
linkList.push({
href,
title
})
// 同时咱们把 a 标签替换成 span 标签
return "<span>";
};
// 笼罩 a 标签的闭标签 token 渲染规定
md.renderer.rules.link_close = function() {if (isWeChatLink) {return "</a>"}
// 咱们会在链接名称前面加上一个上标,代表它存在脚注,上标就是索引
index++
return `<sup>[${index}]</sup></span>`;
};
};
}
export default (md, linkList) => {
// 在外围的规定链上增加咱们的自定义规定
md.core.ruler.push("change-link", makeRule(md, linkList));
};
而后咱们再自行生成脚注 html
字符串,并拼接到 markdown-it
解析后输入的 html
字符串上:
let parseHtml = markdownParser.render(this.props.content.content);
if (linkList.length > 0) {
let linkFootStr = '<div> 援用链接:</div>'
linkList.forEach((item, index) => {linkFootStr += `<div>[${index + 1}] ${item.title}:${item.href}</div>`
})
parseHtml += linkFootStr
}
成果如下:
再欠缺一下款式即可。
同步滚动
编辑区域和预览区域的同步滚动是一个基本功能,首先绑定鼠标移入事件,这样能够判断鼠标是在哪个区域触发的滚动:
// 编辑器
<div id="nice-md-editor" onMouseOver={(e) => this.setCurrentIndex(1, e)}></div>
// 预览区域
<div id="nice-rich-text" onMouseOver={(e) => this.setCurrentIndex(2, e)}></div>
setCurrentIndex(index) {this.index = index;}
而后绑定滚动事件:
// 编辑器
<CodeMirror onScroll={this.handleScroll}></CodeMirror>
// 预览区域容器
<div
id={BOX_ID}
onScroll={this.handleScroll}
ref={(node) => {this.previewContainer = node;}}
>
// 预览区域
<section
id={LAYOUT_ID}
dangerouslySetInnerHTML={{__html: parseHtml,}}
ref={(node) => {this.previewWrap = node;}}
</section>
</div>
handleScroll = () => {if (this.props.navbar.isSyncScroll) {const {markdownEditor} = this.props.content;
const cmData = markdownEditor.getScrollInfo();
// 编辑器的滚动间隔
const editorToTop = cmData.top;
// 编辑器的可滚动高度
const editorScrollHeight = cmData.height - cmData.clientHeight;
// scale = 预览区域的可滚动高度 / 编辑器的可滚动高度
this.scale = (this.previewWrap.offsetHeight - this.previewContainer.offsetHeight + 55) / editorScrollHeight;
// scale = 预览区域的滚动间隔 / 编辑器的滚动间隔 = this.previewContainer.scrollTop / editorToTop
if (this.index === 1) {
// 鼠标在编辑器上触发滚动,预览区域追随滚动
this.previewContainer.scrollTop = editorToTop * this.scale;
} else {
// 鼠标在预览区域触发滚动,编辑器追随滚动
this.editorTop = this.previewContainer.scrollTop / this.scale;
markdownEditor.scrollTo(null, this.editorTop);
}
}
};
计算很简略,依据两个区域的可滚动间隔之比等于两个区域的滚动间隔之比,计算出其中某个区域的滚动间隔,然而这种计算实际上不会很精确,尤其是当存在大量图片时:
能够看到上图中编辑器都滚动到了 4.2 大节,而预览区域 4.2 大节都还看不见。
要解决这个问题单纯的计算高度就不行了,须要能将两边的元素对应起来,预知详情,可参考笔者的另外一篇文章:如何实现一个能准确同步滚动的 Markdown 编辑器。
主题
主题实质上就是 css
款式,markdown
转成 html
后波及到的标签并不是很多,只有全都列举进去定制款式即可。
markdown-nice
首先创立了四个 style
标签:
1.basic-theme
根底主题,定义了一套默认的款式,款式内容能够在 basic.js 文件查看。
2.markdown-theme
用来插入所抉择的主题款式,也就是用来笼罩 basic-theme
的款式,自定义的主题款式也会插入到这个标签:
3.font-theme
用来专门插入字体款式,对应的是这个性能:
// 衬线字体 和 非衬线字体 切换
toggleFont = () => {const {isSerif} = this.state;
const serif = `#nice {font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;}`;
const sansSerif = `#nice {font-family: Roboto, Oxygen, Ubuntu, Cantarell, PingFangSC-light, PingFangTC-light, 'Open Sans', 'Helvetica Neue', sans-serif;}`;
const choosen = isSerif ? serif : sansSerif;
replaceStyle(FONT_THEME_ID, choosen);
message.success("字体切换胜利!");
this.setState({isSerif: !isSerif});
};
4.code-theme
顾名思义,对应的就是用来插入代码块的款式了,markdown-it
提供了一个 highlight
选项来配置代码块高亮,提供一个函数,接管代码字符和语言类型,返回一个 html
片段,也能够包裹 pre
标签后返回,这样 markdown-it
外部就不会再解决。
markdown-nice
应用的是 highlight.js 来实现代码高亮:
export const markdownParser = new MarkdownIt({
html: true,
highlight: (str, lang) => {if (lang === undefined || lang === "") {lang = "bash";}
// 加上 custom 则示意自定义款式,而非微信专属,防止被 remove pre
if (lang && highlightjs.getLanguage(lang)) {
try {
const formatted = highlightjs
.highlight(lang, str, true)
.value.replace(/\n/g, "<br/>") // 换行用 br 示意
.replace(/\s/g, " ") // 用 nbsp 替换空格
.replace(/span /g, "span"); // span 标签修复
return '<pre class="custom"><code class="hljs">' + formatted + "</code></pre>";
} catch (e) {console.log(e);
}
}
// escapeHtml 办法会本义 html 种的 &<>" 字符
return '<pre class="custom"><code class="hljs">' + markdownParser.utils.escapeHtml(str) + "</code></pre>";
},
});
highlight.js
内置了很多主题:styles,markdown-nice
从中挑了 6 种:
并且还反对 mac
格调,区别就是 mac
格调减少了下列款式:
一键复制
markdown-nice
有三个一键复制的按钮,别离是 公众号
、 知乎
、 掘金
,掘金当初自身编辑器就是markdown
的,所以咱们间接疏忽。
公众号:
copyWechat = () => {const layout = document.getElementById(LAYOUT_ID); // 爱护现场
const html = layout.innerHTML;
solveWeChatMath();
this.html = solveHtml();
copySafari(this.html);
message.success("已复制,请到微信公众平台粘贴");
layout.innerHTML = html; // 复原现场
};
知乎:
copyZhihu = () => {const layout = document.getElementById(LAYOUT_ID); // 爱护现场
const html = layout.innerHTML;
solveZhihuMath();
this.html = solveHtml();
copySafari(this.html);
message.success("已复制,请到知乎粘贴");
layout.innerHTML = html; // 复原现场
};
次要的区别其实就是 solveWeChatMath
和solveZhihuMath
办法,这两个办法是用来解决公式的问题。markdown-nice
应用 MathJax 来渲染公式(各位本人看,笔者对 MathJax
不相熟,属实看不懂~):
try {
window.MathJax = {
tex: {inlineMath: [["\$", "\$"]],// 行内公式的开始 / 完结分隔符
displayMath: [["\$\$", "\$\$"]],// 块级公式的开始 / 完结分隔符
tags: "ams",
},
svg: {fontCache: "none",// 不缓存 svg 门路,不进行复用},
options: {
renderActions: {addMenu: [0, "",""],
addContainer: [
190,
(doc) => {for (const math of doc.math) {this.addContainer(math, doc);
}
},
this.addContainer,
],
},
},
};
require("mathjax/es5/tex-svg-full");
} catch (e) {console.log(e);
}
addContainer(math, doc) {
const tag = "span";
const spanClass = math.display ? "span-block-equation" : "span-inline-equation";
const cls = math.display ? "block-equation" : "inline-equation";
math.typesetRoot.className = cls;
math.typesetRoot.setAttribute(MJX_DATA_FORMULA, math.math);
math.typesetRoot.setAttribute(MJX_DATA_FORMULA_TYPE, cls);
math.typesetRoot = doc.adaptor.node(tag, {class: spanClass, style: "cursor:pointer"}, [math.typesetRoot]);
}
// 内容更新后调用下列办法从新渲染公式
export const updateMathjax = () => {window.MathJax.texReset();
window.MathJax.typesetClear();
window.MathJax.typesetPromise();};
公式转换的 html
构造如下:
公众号编辑器不反对公式,所以是通过直接插入svg
:
export const solveWeChatMath = () => {const layout = document.getElementById(LAYOUT_ID);
// 获取到所有公式标签
const mjxs = layout.getElementsByTagName("mjx-container");
for (let i = 0; i < mjxs.length; i++) {const mjx = mjxs[i];
if (!mjx.hasAttribute("jax")) {break;}
// 移除 mjx-container 标签上的一些属性
mjx.removeAttribute("jax");
mjx.removeAttribute("display");
mjx.removeAttribute("tabindex");
mjx.removeAttribute("ctxtmenu_counter");
// 第一个节点为 svg 节点
const svg = mjx.firstChild;
// 将 svg 通过属性设置的宽高改成通过款式进行设置
const width = svg.getAttribute("width");
const height = svg.getAttribute("height");
svg.removeAttribute("width");
svg.removeAttribute("height");
svg.style.width = width;
svg.style.height = height;
}
};
知乎编辑器反对公式,所以会间接把公式相干的 html
替换为 img
标签:
export const solveZhihuMath = () => {const layout = document.getElementById(LAYOUT_ID);
const mjxs = layout.getElementsByTagName("mjx-container");
while (mjxs.length > 0) {const mjx = mjxs[0];
let data = mjx.getAttribute(MJX_DATA_FORMULA);
if (!data) {continue;}
if (mjx.hasAttribute("display") && data.indexOf("\\tag") === -1) {data += "\\\\";}
// 替换整个公式标签
mjx.outerHTML = '<img class="Formula-image"data-eeimg="true"src="" alt="'+ data +'">';
}
};
解决完公式后接下来会执行 solveHtml
办法:
import juice from "juice";
export const solveHtml = () => {const element = document.getElementById(BOX_ID);
let html = element.innerHTML;
// 将公式的容器标签替换成 span
html = html.replace(/<mjx-container (class="inline.+?)<\/mjx-container>/g,"<span $1</span>");
// 将空格替换成
html = html.replace(/\s<span class="inline/g,' <span class="inline');
// 同上
html = html.replace(/svg><\/span>\s/g, "svg></span> ");
// 这个标签下面曾经替换过了,这里为什么还要再替换一遍
html = html.replace(/mjx-container/g, "section");
html = html.replace(/class="mjx-solid"/g, 'fill="none"stroke-width="70"');
// 去掉公式的 mjx-assistive-mml 标签
html = html.replace(/<mjx-assistive-mml.+?<\/mjx-assistive-mml>/g, "");
// 获取四个款式标签内的款式
const basicStyle = document.getElementById(BASIC_THEME_ID).innerText;
const markdownStyle = document.getElementById(MARKDOWN_THEME_ID).innerText;
const codeStyle = document.getElementById(CODE_THEME_ID).innerText;
const fontStyle = document.getElementById(FONT_THEME_ID).innerText;
let res = "";
try {
// 应用 juice 库将款式内联到 html 标签上
res = juice.inlineContent(html, basicStyle + markdownStyle + codeStyle + fontStyle, {
inlinePseudoElements: true,// 插入伪元素,做法是转换成 span 标签
preserveImportant: true,// 放弃!import
});
} catch (e) {message.error("请查看 CSS 文件是否编写正确!");
}
return res;
};
这一步次要是替换掉公式的相干标签,而后获取了四个款式标签内的款式,最要害的一步是最初应用 juice 将款式内联到了 html
标签里,所以预览的时候款式是拆散的,然而最终咱们复制进去的数据是带款式的:
html
处理完毕,最初会执行复制到剪贴板的操作copySafari
:
export const copySafari = (text) => {
// 获取 input
let input = document.getElementById("copy-input");
if (!input) {
// input 不能用 CSS 暗藏,必须在页面内存在。input = document.createElement("input");
input.id = "copy-input";
input.style.position = "absolute";
input.style.left = "-1000px";
input.style.zIndex = "-1000";
document.body.appendChild(input);
}
// 让 input 选中一个字符,无所谓那个字符
input.value = "NOTHING";
input.setSelectionRange(0, 1);
input.focus();
// 复制触发
document.addEventListener("copy", function copyCall(e) {e.preventDefault();
e.clipboardData.setData("text/html", text);
e.clipboardData.setData("text/plain", text);
document.removeEventListener("copy", copyCall);
});
document.execCommand("copy");
};
导出为 PDF
导出为 PDF
性能实际上是通过打印性能实现的,也就是调用:
window.print();
能够看到打印的内容只有预览区域,这是怎么实现的呢,很简略,通过媒体查问,在打印模式下暗藏掉不须要打印的其余元素即可:
@media print {
.nice-md-editing {display: none;}
.nice-navbar {display: none;}
.nice-sidebar {display: none;}
.nice-wx-box {
overflow: visible;
box-shadow: none;
width: 100%;
}
.nice-style-editing {display: none;}
#nice-rich-text {padding: 0 !important;}
.nice-footer-container {display: none;}
}
成果就是这样的:
总结
本文通过源码的角度简略理解了一下 markdown-nice
的实现原理,整体逻辑比较简单,有些细节上的实现还是有点麻烦的,比方扩大 markdown-it
、对数学公式的反对等。扩大markdown-it
的场景还是有很多的,比方 VuePress 大量的性能都是通过写 markdown-it
插件来实现的,所以有相干的开发需要能够参考一下这些优良开源我的项目的实现。