I usually use Markdown
for writing articles, but when I publish it, I will encounter some platforms that do not support Markdown
Rearrangement is impossible, so I will use it. Some Markdown
tools for converting rich text, such as markdown-nice , if you use it more, you will be curious about how it is implemented, so there is this article.
markdown-nice
is a project based on React
, let's take a look at its overall page:
A toolbar at the top with three side-by-side areas in the middle, namely the editing area, the preview area, and the custom theme area. The custom theme area is hidden by default.
Basically it is a Markdown
editor, with some adaptations for each platform added.
editor
The editor uses CodeMirror , specifically a secondary encapsulated component 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}
/>
)
}
}
Shortcuts, Commands
markdown-nice
Set some shortcut keys through the extraKeys
option, and also add some shortcut buttons in the toolbar:
The logic of these shortcut keys or command buttons to operate the text content is basically the same, first get the content of the current selection:
const selected = editor.getSelection()
Then make the processing modification:
`**${selected}**`
Finally replace the content of the selection:
editor.replaceSelection(`**${selected}**`)
In addition, you can also modify the position of the cursor to improve the experience. For example, after the bold operation, the cursor position will be behind the text, instead of *
because markdown-nice
will be modified after replacing the content of the selection the position of the cursor:
export const bold = (editor, selection) => {
editor.replaceSelection(`**${selection}**`);
const cursor = editor.getCursor();
cursor.ch -= 2;// 光标位置向前两个字符
editor.setCursor(cursor);
};
sheet
The table syntax of Markdown
is more troublesome to write by hand, markdown-nice
For the table, it only provides the function of inserting table syntax symbols for you, you can enter the number of table rows and columns to be inserted:
After confirmation, the symbol will be inserted automatically:
The implementation is actually the splicing logic of a string:
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");
};
After the table characters are generated, the content of the current selection can be replaced:
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();
};
Also modifies the cursor position and refocuses the editor.
upload picture
markdown-nice
支持直接拖动图片到编辑区域进行上传和粘贴图片直接上传,这是通过监听CodeMirror
编辑器的drop
paste
implementation:
<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});
}
}
}
It is judged that if there is a file in the dragged or pasted data, the uploadAdaptor
method will be called:
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);
}
}
Other types of image beds are omitted. Taking Alibaba Cloud OSS
as an example, it will first check whether the relevant configuration exists. If it exists, it will call the aliOSSUpload
method:
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});
};
};
};
This step is mainly to convert the file type to arrayBuffer
type, and finally call aliOSSPutObject
to upload the file:
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);
});
};
After the upload is successful, the image will be inserted into the document:
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());
}
The specific upload logic of other major platforms can refer to the source code: imageHosting.js .
Format Markdown
markdown-nice
support the function of formatting Markdown
, that is, beautification function, such as:
After beautification:
Formatting is using 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;
};
preview
The preview is to convert Markdown
to html
for display. The preview area only needs to provide a container element, such as div
, and then convert the converted html
Content use div.innerHTML = html
method can be added.
Markdown
转换为html
,比如markdown-it 、 marked 、 showdown , markdown-nice
使用的是markdown-it
.
Core code:
const parseHtml = markdownParser.render(this.props.content.content);
return (
<section
dangerouslySetInnerHTML={{
__html: parseHtml,
}}
/>
)
markdownParser
ie markdown-it
Example:
import MarkdownIt from "markdown-it";
export const markdownParser = new MarkdownIt({
html: true,// 允许在源代码中存在HTML标签
highlight: (str, lang) => {
// 代码高亮逻辑,后面再看
},
});
plugin
After creating an instance of MarkdownIt
, many plugins were registered:
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);
It is also reflected in the function comments of the plugin.
markdown-it
Markdown
字符串转成一个个token
, token
html
A string, such as # 街角小林
will generate the following token
list (with some fields removed):
[
{
"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
These rules will execute the above rules in order on the incoming markdown
string, which contains block
and inlnie
The execution process of the rules, block
and inline
related rules are used to generate a token
, as the name suggests, a token
responsible for generating block-level types token
, such as title, code block, table, item list, etc., one responsible for generating inline types after block-level elements are generated token
, such as text, links, pictures, etc.
block
will scan line by line at runtime markdown
string, all block levels will be executed for each line of string in turn rule
function, parsing and generating block level token
, block
规则有table
、 code
、 fence
、 blockquote
、 hr
, list
, heading
, paragraph
, etc.
block
类型的规则处理完之后,可能会生成一种---22b077df8379b8ea41b06faf7878a9a7 type
inline
的token
,这种token
属于未完全解析的token
, inline
类型的token
,也就是对块级token
的content
解析生成内联token
, inline
规则有text
、 link
, image
etc.
After these parsing rules are executed, an array of token
will be output, and then a render
related rule will be generated html
string, so a markdown-it
If the plugin wants to interfere with the generated token
, then update, expand, and add different types of parsing rule
, if you want to interfere with the generated html
token
------ html
, then by updating, extending, adding rendering rule
.
The above is just a rough introduction. Those who are interested in in-depth understanding can read markdown-it
source code or the following two series of articles:
markdown-it source code analysis 1-overall process , markdown-it series of articles
markdown-nice
There are so many plugins used, some are from the community, some are written by yourself, let's take a look at two of them that are relatively simple.
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));
};
This plugin is very simple. When there are multiple nested blockquote
, add a class name to the outermost blockquote token
, the effect is as follows:
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));
};
This plugin is even simpler, overwriting the built-in list_item
rule, the effect is to add a ---6142f0a4fd4d8b234b148e9af527eeb9 li
tag to the section
tag.
External link to footnote
We all know that the biggest limitation of the official account is that hyperlinks are only allowed in the whitelist, and others will be filtered out, so if we don’t do anything, our hyperlinks will be gone. The solution is generally to convert them into footnotes and display At the end of the article, markdown-nice
The logic to achieve this is more complicated, and will first change the content of Markdown
to:
[理想青年实验室](http://lxqnsys.com/)
formatted as:
[理想青年实验室](http://lxqnsys.com/ "理想青年实验室")
That is to add the title, and then pass the markdown-it
plugin processing token
to generate a footnote:
markdownParser
.use(markdownItLinkfoot) // 修改脚注
The implementation of this plug-in is also more complicated. Those who are interested can read the source code: markdown-it-linkfoot.js .
In fact, we can choose another relatively simple idea, we can cover the markdown-it
internal link token
rendering rules, and collect all the link data at the same time, and finally we generate it ourselves html
string spliced to markdown-it
output html
string.
For example, we create a markdownItLinkfoot2
plugin, register:
// 用来收集所有的链接
export const linkList = []
markdownParser
.use(markdownItLinkfoot2, linkList)
Pass the array of collection links to the plugin through options, followed by the plugin's code:
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));
};
Then we generate the footnote html
string by ourselves, and splicing it to the markdown-it
output after parsing html
string:
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
}
The effect is as follows:
Just refine the style.
Synchronized scrolling
The synchronous scrolling of the editing area and the preview area is a basic function. First, bind the mouse move-in event, so that you can determine which area the mouse is in which the scroll is triggered:
// 编辑器
<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;
}
Then bind the scroll event:
// 编辑器
<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);
}
}
};
The calculation is very simple. According to the ratio of the scrollable distances of the two areas equal to the ratio of the scrollable distances of the two areas, the scrolling distance of one of the areas is calculated, but this calculation is actually not very accurate, especially when there are a large number of When pictured:
It can be seen that the editor in the above figure has scrolled to section 4.2, and the preview area of section 4.2 is still invisible.
To solve this problem, it is not enough to simply calculate the height. It is necessary to be able to correspond the elements on both sides, and to predict the details, you can refer to another article of the author: How to implement a Markdown editor that can scroll accurately and synchronously .
theme
The theme is essentially the css
style, and the markdown
is converted into html
and there are not many tags involved, as long as all of them are listed to customize the style.
markdown-nice
Four style
tags are created first:
1. basic-theme
The basic theme defines a set of default styles. The style content can be viewed in the basic.js file.
2. markdown-theme
Used to insert the selected theme style, which is used to override the style of basic-theme
. The custom theme style will also be inserted into this tag:
3. font-theme
Used to specifically insert font styles, corresponding to this function:
// 衬线字体 和 非衬线字体 切换
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
As the name suggests, it corresponds to the style used to insert code blocks, markdown-it
provides a highlight
option to configure code block highlighting, provides a function to receive code characters and language types, Returns a html
fragment, or wraps the pre
tag and returns it, so that markdown-it
will not be processed internally.
markdown-nice
Use highlight.js to achieve code highlighting:
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
There are many built-in themes: styles , markdown-nice
6 from them:
And also supports the mac
style, the difference is that the mac
style adds the following styles:
One-click copy
markdown-nice
有三个一键复制的按钮,分别是公众号
、 知乎
、 掘金
,掘金现在本身编辑器就是markdown
, so we ignore it directly.
the public:
copyWechat = () => {
const layout = document.getElementById(LAYOUT_ID); // 保护现场
const html = layout.innerHTML;
solveWeChatMath();
this.html = solveHtml();
copySafari(this.html);
message.success("已复制,请到微信公众平台粘贴");
layout.innerHTML = html; // 恢复现场
};
Know almost:
copyZhihu = () => {
const layout = document.getElementById(LAYOUT_ID); // 保护现场
const html = layout.innerHTML;
solveZhihuMath();
this.html = solveHtml();
copySafari(this.html);
message.success("已复制,请到知乎粘贴");
layout.innerHTML = html; // 恢复现场
};
The main difference is actually the solveWeChatMath
and solveZhihuMath
methods, which are used to solve the problem of formulas. markdown-nice
Use MathJax to render the formula (see for yourself, the author is not familiar with MathJax
, it is true that I can't understand~):
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();
};
The converted formula html
structure is as follows:
The official account editor does not support formulas, so by directly inserting 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;
}
};
The Zhihu editor supports formulas, so it will directly replace the formula-related html
with img
tags:
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 + '">';
}
};
After processing the formula, it will execute the solveHtml
method:
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;
};
This step is mainly to replace the relevant tags of the formula, and then obtain the styles in the four style tags. The most critical step is to use juice to inline the styles into the html
tag, so the style is previewed. is separated, but in the end the data we copy out is styled:
html
After processing, the operation of copying to the clipboard will be performed at the end 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");
};
Export to PDF
Exported as PDF
function is actually implemented by the print function, which is to call:
window.print();
You can see that the printed content is only the preview area. How is this achieved? It is very simple. Through media query, you can hide other elements that do not need to be printed in print mode:
@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;
}
}
The effect is this:
Summarize
This article briefly understands the implementation principle of markdown-nice
from the perspective of source code. The overall logic is relatively simple, and the implementation of some details is still a bit troublesome, such as extension markdown-it
, support for mathematical formulas Wait. There are still many scenarios for extending markdown-it
. For example, a large number of VuePress functions are implemented by writing markdown-it
plug-ins, so if you have relevant development requirements, you can refer to these excellent open source projects. accomplish.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。