在vue中使用marked解析MARKDOWN,生成目录,运行代码示例
前言对于我来说一个博客系统就是用来总结自己所学得知识的。写写文章,巩固技术,写文章我就采用了mavon-editor,在后台将写好的Markdown文章保存到数据库里,前台在获取Markdown文章将其解析成html代码然后渲染。所以写文章不用愁了,那如何解析Markdown呢!我前前后后用了mavon-editor(包太大),vue-marked(功能少)等等插件来实现!结果不满足预期。所以不如
个人博客原文地址https://gitee.com/baymaxsjj
前言
对于我来说一个博客系统就是用来总结自己所学得知识的。写写文章,巩固技术,写文章我就采用了mavon-editor
,在后台将写好的Markdown文章保存到数据库里,前台在获取Markdown文章将其解析成html代码然后渲染。所以写文章不用愁了,那如何解析Markdown呢!我前前后后用了mavon-editor
(包太大),vue-marked
(功能少)等等插件来实现!结果不满足预期。所以不如使用marked.js直接解析呢!包小效率高,于是就对marked.js进行封装,实现了目录,运行代码块,图片查看等功能!已经能满足了基本的需求。
marked.js
一个功能齐全的markdown解析器和编译器,用JavaScript编写。 专为速度而设计。marked.js官网
- 快速构建
- 用于解析markdown的低级编译器,无需长时间缓存或阻塞
- 非常轻量,同时实现支持的falses和规格的所有降价功能
- 支持浏览器,服务器或命令行界面(CLI)
安装
npm install marked --save
//在vue组件中导入
import marked from 'marked'
使用
//markdownString:要解析的markdown,必须为字符串
//options:marked.js的配置
//callback:回调函数。I如果 options 参数没有定义,它就是第二个参数。
marked(markdownString [,options] [,callback])
基本配置
marked.setOptions({
renderer: rendererMD,
gfm: true,//默认为true。 允许 Git Hub标准的markdown.
tables: true,//默认为true。 允许支持表格语法。该选项要求 gfm 为true。
breaks: false,//默认为false。 允许回车换行。该选项要求 gfm 为true。
pedantic: false,//默认为false。 尽可能地兼容 markdown.pl的晦涩部分。不纠正原始模型任何的不良行为和错误。
sanitize: false,//对输出进行过滤(清理)
smartLists: true,
smartypants: false//使用更为时髦的标点,比如在引用语法中加入破折号。
});
实现目录
实现目录功能,网上又很多的写法!像我这样的小白也看不懂,代码都好长。我实现的过程肯有些投机取巧了。实现的过程也很简单,没有正则表达,没有复杂的代码也就几行代码吧!
实现原理
看看下面这张图,观察一下标题和目录有哪些相同之处。
其实从上往下看没有什么不同,从左往右看也就是出现了缩进。
所以我的目录实现原理,将所以的标题提取出来,然后根据其标题大小进行缩进。
自定义渲染方式
知道思路后,改如何实现呢!通过marked.js文档,我们可以重写renderer(渲染),
let rendererMD = new marked.Renderer();
let that=this
/*
重写标题
text:标题文本
level:标签
*/
rendererMD.heading = function(text, level, raw) {
//保存这篇文章的最大标签
if(level<that.maxTitle){
that.maxTitle=level
}
anchor+=1
/*
toc:数组用于保存标题,
id:标题id,用于点击目录滚动到改标题
tag:记录属于那个标签(h1……h6)
test:标签内容
*/
that.toc.push(
{
'id':anchor,
'tag':level,
'text':text
}
)
return `<h${level} id="toc-nav${anchor}">${text}</h${level}>`;
};
//重写a标签,在新标签打开
rendererMD.link = function(href,title,text){
return '<a href="'+href+'" title="'+text+'" target="_blank">'+text+'</a>';
}
//更多规则到marked.js官网查看
<ul >
<li v-for="item of toc" :key="item.id" @click="toTarget(item.id)" :style="{'padding-left':item.tag-maxTitle+'em'}" v-html="item.text">
</li>
</ul>
为什么保存最大标题
通过也是渲染成功后,如果没有没有最大标题,假如文章只有h6标题,那么目录还是会缩进6个字符,不好看,这样做的目的就是为了保证所有的标题都是从最大的以下开始缩进。缩进利用的padding-left。item.tag-maxTitle
也好理解:
//最大标题从h1开始 //最大标题从h4开始
h1->0em h4->0em
h2->1em h5->1em
…… ……
上面可以看出最大标签始终为0em,其它标签都是相对最大标签的偏移。
运行代码
像我的博客,就可以运行一些代码示例来展示,主要原理就是通过Components 定义一个运行代码的标签。通过marked.js 解析代码块,将特点语言的代码块提取出来(我这里就是将demo 标记的语言提取出来),然后拼接成自定义的标签。
rendererMD.code = function (code, language) {
// 提取language标识为 demo 的代码块重写
if (language === 'demo') {
DEMO_UID+=2
// 页面中可能会有很多的示例,这里增加ID标识
const id = 'demo-mobai-template-' + (DEMO_UID)
// 将代码内容保存在template标签中
const template = `<template type="text/demo" id="${id}">${code}</template>`
// 将template和自定义标签通过ID关联
const sandbox = `<demo-mobai template="${id}"></demo-mobai>`
// 返回新的HTML
return template + sandbox
}
}
上面解决了标签问题,接下来就是解析标签,以下的大部分代码参考自水墨寒,我修改了部分代码,主要解析了两个问题,
一是,默认的运行代码会有一个样式,我通过id号区分要显示和不要显示的,
二是,在我用的时候发现不能引入在线的js,只能运行代码块中的代码,这样就不太好了,比如我要用一些框架,比我来说我的这篇文章,vue 音乐播放器,就能运行在线的vue 框架和element ui,这个问题是由于引入的js代码后执行,所以不能解析,我的解决办法就是通过Promise当js加载成功后,resolve();添加到Promise数组中,通过Promise.all(Promise数组)当所有js 都加载成功后在将代码块中的代码添加到Shadow DOM中。
let arr=[]
// 4. 拼合所有Script
for(let i=0;i<scripts.length;i++){
// 全局替换document为新的$shadowDocument
if(scripts[i].src){
// 创建
const $sc = document.createElement('script')
$sc.setAttribute("type", "text/javascript");
$sc.setAttribute('src', scripts[i].src);
this.shadow.appendChild($sc)
arr.push(
//通过Promise来解决,所有js都加载成功后,在将代码添加到Shadow DOM
new Promise(function(resolve,reject){
//js 加载完成回执行
$sc.onload = function() {
console.log($sc)
resolve();
};
})
)
this.shadow.getElementById('demo-run').removeChild(scripts[i])
continue
}
$globalDefines.innerHTML += `{
${scripts[i].textContent.replace(/(document)\.(getElementById|querySelector|querySelectorAll|getElementsByClassName|getElementsByName|getElementsByTagName)/gm, '$shadowDocument.$2').replace(/\r\n?/gm, '')}
}`
// 移除旧节点
this.shadow.getElementById('demo-run').removeChild(scripts[i])
}
$globalDefines.innerHTML += `})();`
Promise.all(arr).then(()=>{
console.log('js加载成功');
this.shadow.appendChild($globalDefines)
})
Web Components 标准非常重要的一个特性是,它使开发者能够将HTML页面的功能封装为 custom elements(自定义标签),而往常开发者不得不写一大堆冗长、深层嵌套的标签来实现同样的页面功能
首先要掌握两个知识点,Components 和Shadow DOM详情参考MDN
这两个我就不过多说,其实我也不太会,也没MDN说的细,不过使用的要谨慎,有兼容问题,
下面的代码才是关键,上面已经将特定语言的代码快转化成自定义标签,通过marked.js 渲染到页面上了,但并不起作用,因为浏览器识别不出改标签,下面通过Components 定义一个标签,然后通过Shadow ,以下是我博客中解析MARKDOWN的一个组件,其中使用到一个vue-dompurify-html用了对MARKDOWN过滤防止恶意代码,
<template>
<div class="marked">
<div ref="preview" class="write">
//没有vue-dompurify-html,可以将v-dompurify-html="html"改成v-html="html"
<span
v-if="dompurify"
v-dompurify-html="html"
></span>
<span
v-else
v-html="html"
></span>
//没有使用element ui 可以将下面删除
<el-image
v-if="imgView"
id="imgview"
style="height:0px"
:src="url"
:preview-src-list="srcList">
</el-image>
</div>
<transition name="slide-fade">
<div class="toc" v-if="tocNav&&toc.length" v-show="tocIsShow">
<div class="toc-top a-tag">
<span class="toc-title">TOC</span>
<a href="javascript:;" class="toc-close" @click="tocIsShow=false">「 关闭 」</a>
</div>
<ul >
<li v-for="item of toc" :key="item.id" @click="toTarget(item.id)" :style="{'padding-left':item.tag-maxTitle+'em'}" v-html="item.text">
</li>
</ul>
</div>
</transition>
<transition name="slide-fade">
<div class="toc-tag" v-if="tocNav &&toc.length" v-show="!tocIsShow" @click="tocIsShow=true">
<i></i>
<i></i>
<i></i>
</div>
</transition>
</div>
</template>
<script>
import marked from 'marked'
import hljs from '@/utils/highlight.min.js'
import { Notification } from 'element-ui';
let rendererMD = new marked.Renderer();
const TAG_NAME = 'demo-mobai'
let Deom=true;
try {
// 此处是可能产生例外的语句
customElements.define(TAG_NAME, class DemoSandbox extends HTMLElement {
constructor() {
super()
// 使用影子DOM
this.shadow = this.attachShadow({
mode: 'open'
})
// 获取关联的代码块模板的ID
const templateId = this.getAttribute('template')
const $template = document.getElementById(templateId)
if (!templateId) {
return
}
// 获取代码块内容
const template = $template.innerHTML
console.log(templateId)
let id=parseInt(templateId.split('demo-mobai-template-')[1]);
console.log(id%2==0)
if(id%2==0){
// 用获取到的代码块来填充影子DOM的HTML
let code=marked('```html \n'+template+'\n```', {
sanitize: false,
highlight: function (code) {
return hljs.highlightAuto(code).value;
},
})
this.shadow.innerHTML =`
<style>
:host {
display:block;
width:100%;
padding: 0;
border: 1px solid #f0f0f0;
color: #414240;
font-size: 1rem;
position: relative;
margin: 10px 0;
min-height: 36px;
}
:host:before {
content: " ";
position: absolute;
-webkit-border-radis: 50%;
border-radius: 50%;
background: #ff6058;
width: 12px;
height: 12px;
left: 15px;
margin-top: 10px;
-webkit-box-shadow: 20px 0 #ffbd2b, 40px 0 #3cef57;
box-shadow: 20px 0 #ffbd2b, 40px 0 #3cef57;
z-index: 2;
}
:host:after {
content: "demo";
position: absolute;
top:0px;
left: 50%;
z-index: 2;
color:var(--main-6);
font-weight:bold;
transform: translateX(-50%);
font-size: 20px;
line-height:32px
}
* {
box-sizing: border-box;
}
#demo-run {
padding:20px;
background-color:white;
border-top: 32px solid #ecf5ff;
border-radius: 6px;
overflow-x: auto;
overflow-y: hidden;
position:relative;
}
#demo-code {
padding:20px;
border-top: 1px solid #eaeefb;
font-size: 85%;
font-family: "Operator Mono SSm A","Operator Mono SSm B","Operator Mono","Source Code Pro",Menlo,Consolas,Monaco,monospace;
line-height: 1.4;
background-color:#fefefe;
}
#demo-code code{
display: block;
overflow-x: auto;
}
#demo-open {
width:100%;
-webkit-appearance: none;
border:none;
border-top: 1px solid #eaeefb;
text-align:center;
padding: 10px 20px;
font-size: 14px;
cursor: pointer;
outline: 0;
transition: background-color .3s;
color: var(--main-6);
background-color:#fff
}
#demo-open:hover,
#demo-open:active {
background-color: var(--main-9);
}
</style>
<div id="demo-run">${template}</div>
<div id="demo-code" hidden>${code}</div>
<button id="demo-open">查看源码</button>
<style>
.hljs{display:block;overflow-x:auto}.hljs-comment,.hljs-meta{color:#969896}.hljs-emphasis,.hljs-quote,.hljs-string,.hljs-strong,.hljs-template-variable,.hljs-variable{color:#df5000}.hljs-keyword,.hljs-selector-tag,.hljs-type{color:#a71d5d}.hljs-attribute,.hljs-bullet,.hljs-literal,.hljs-number,.hljs-symbol{color:#0086b3}.hljs-name,.hljs-section{color:#63a35c}.hljs-tag{color:#333}.hljs-attr,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-title{color:#795da3}.hljs-addition{color:#55a532;background-color:#eaffea}.hljs-deletion{color:#bd2c00;background-color:#ffecec}.hljs-link{text-decoration:underline}.hljs-comment,.hljs-quote{color:#998}.hljs-keyword,.hljs-selector-tag,.hljs-subst{font-weight:700}.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal}.hljs-doctag,.hljs-string{color:#d14}.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:700}.hljs-subst{font-weight:400}.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:700}.hljs-link,.hljs-regexp{color:#009926}.hljs-bullet,.hljs-symbol{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}
</style>
`
const co= this.shadow.getElementById("demo-code")
this.shadow.getElementById("demo-open").addEventListener(
"click", (function() {
co.hasAttribute("hidden") ? co.removeAttribute("hidden") : co.setAttribute("hidden", "")
}));
}else {
this.shadow.innerHTML =`
<div id="demo-run">${template}</div>
`
}
// 移除掉关联的template节点
// 移除掉关联的template节点
$template.parentNode.removeChild($template)
// 处理 script
// 1. 查找影子DOM中刚才填充的script节点
const scripts = Array.from(this.shadow.querySelectorAll('script'))
console.log(scripts)
// 2. 创建一个用来保存影子DOM根节点的Script
const $globalDefines = document.createElement('script')
// 3. 创建一个自执行函数,将代码包裹起来
$globalDefines.innerHTML = `(function(){
const $component = document.querySelector('${TAG_NAME}[template="${templateId}"]');
const $shadowDocument = $component.shadowRoot;
`
let arr=[]
// 4. 拼合所有Script
for(let i=0;i<scripts.length;i++){
// 全局替换document为新的$shadowDocument
if(scripts[i].src){
// 创建
const $sc = document.createElement('script')
$sc.setAttribute("type", "text/javascript");
$sc.setAttribute('src', scripts[i].src);
this.shadow.appendChild($sc)
arr.push(
//通过Promise来解决,所有js都加载成功后,在将代码添加到Shadow DOM
new Promise(function(resolve,reject){
//js 加载完成回执行
$sc.onload = function() {
console.log($sc)
resolve();
};
})
)
this.shadow.getElementById('demo-run').removeChild(scripts[i])
continue
}
$globalDefines.innerHTML += `{
${scripts[i].textContent.replace(/(document)\.(getElementById|querySelector|querySelectorAll|getElementsByClassName|getElementsByName|getElementsByTagName)/gm, '$shadowDocument.$2').replace(/\r\n?/gm, '')}
}`
// 移除旧节点
this.shadow.getElementById('demo-run').removeChild(scripts[i])
}
$globalDefines.innerHTML += `})();`
Promise.all(arr).then(()=>{
console.log('js加载成功');
this.shadow.appendChild($globalDefines)
})
}
})
} catch(error) {
Deom=false
Notification.error({
title: '浏览器不支持该功能',
message: '请使用最新浏览器',
})
}
export default {
name: 'MyMarked',
props: {
initialValue: {
// 初始化内容
type: String,
default: ''
},
markedOptions: {
type: Object,
default: () => ({})
},
copyCode: {// 复制代码
type: Boolean,
default: true
},
dompurify:{
type:Boolean,
default:true
},
copyBtnText: {// 复制代码按钮文字
type: String,
default: '复制代码'
},
imgView:{
type: Boolean,
default: true
},
tocNav:{
type: Boolean,
default: false
},
},
data() {
return {
html: '',
previewImgModal: false,
previewImgSrc: '',
previewImgMode: '',
toc:[],
tocIsShow:document.body.clientWidth>600?true:false,
maxTitle:6,
url:'https://iconfont.alicdn.com/t/43f13cdf-39c8-4053-affd-b2d3e75b1e0e.png',
srcList: [
'https://iconfont.alicdn.com/t/43f13cdf-39c8-4053-affd-b2d3e75b1e0e.png',
'https://iconfont.alicdn.com/t/9d79fc67-6f0d-4af2-90e7-ce50ef4404b7.png'
]
};
},
mounted() {
this.translateMarkdown();
},
methods: {
translateMarkdown() {
let that=this
let DEMO_UID = 0
let SHOW_UID=0
rendererMD.code = function (code, language) {
// 提取language标识为 demo 的代码块重写
if(Deom){
if (language === 'demo') {
DEMO_UID+=2
// 页面中可能会有很多的示例,这里增加ID标识
const id = 'demo-mobai-template-' + (DEMO_UID)
// 将代码内容保存在template标签中
const template = `<template type="text/demo" id="${id}">${code}</template>`
// 将template和自定义标签通过ID关联
const sandbox = `<demo-mobai template="${id}"></demo-mobai>`
// 返回新的HTML
return template + sandbox
}
if(language === 'show'){
// 页面中可能会有很多的示例,这里增加ID标识
const id = 'demo-mobai-template-' + (++SHOW_UID)
// 将代码内容保存在template标签中
const template = `<template type="text/demo" id="${id}">${code}</template>`
// 将template和自定义标签通过ID关联
const sandbox = `<demo-mobai template="${id}"></demo-mobai>`
// 返回新的HTML
return template + sandbox
}
}else{
if (language === 'demo') {
language='html';
}
}
// 其他标识的代码块依然使用代码高亮显示
return `<div class="code-block"><span class="code-language">${language}</span><span class="copy-code el-icon-files">${that.copyBtnText}</span><pre rel="${language}"><code class="hljs ${language}">${hljs.highlightAuto(code).value}</code></pre></div>`
}
rendererMD.link = function(href,title,text){
return '<a href="'+href+'" title="'+text+'" target="_blank">'+text+'</a>';
}
let anchor=0;
if(that.tocNav){
rendererMD.heading = function(text, level, raw) {
// const anchor = tocify.add(text, level);
if(level<that.maxTitle){
that.maxTitle=level
}
anchor+=1
that.toc.push(
{
'id':anchor,
'tag':level,
'text':text
}
)
return `<h${level} id="toc-nav${anchor}">${text}</h${level}>`;
};
}
// customElements.define(TAG_NAME, Demobox)
let html = marked(this.initialValue, {
sanitize: false,
renderer: rendererMD,
...this.markedOptions
})
this.html = html;
// this.addCopyListener();
if(this.imgView){
this.addImageClickListener();
}
},
addImageClickListener() {// 监听查看大图
const {imgs = []} = this;
if (imgs.length > 0) {
for (let i = 0, len = imgs.length; i < len; i++) {
imgs[i].onclick = null;
}
}
setTimeout(() => {
this.imgs = this.$refs.preview.querySelectorAll('img');
for (let i = 0, len = this.imgs.length; i < len; i++) {
this.imgs[i].onclick = () => {
const src = this.imgs[i].getAttribute('src');
this.srcList[1]=src
this.url=src
setTimeout(() => {
document.getElementById("imgview").click()
},5)
};
}
}, 1000);
},
toTarget(target){
target='#toc-nav'+target
let toElement = document.querySelector(target);
toElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
})
},
},
watch: {
initialValue() {
this.translateMarkdown();
}
},
destroyed () {
window.removeEventListener('scroll', this.scroll, false)
},
};
</script>
<style lang="stylus" scoped>
@import '~@/assets/style/marked.css'
.marked
display: flex;
flex-flow: row nowrap;
position: relative;
align-items: flex-start;
.write
flex: 1 1 auto;
width: 1%;
overflow: hidden;
.toc
width: 220px;
margin-left: 20px;
border-left: 1px solid #efefee
position: sticky;
top: 100px;
flex-shrink: 0;
padding-left 10px
.toc-top
display: flex;
justify-content: space-between;
align-items center
padding 10px 0
.toc-title
font-size 18px
&:before
content '#'
color var(--main-6)
padding-right 3px
.toc-close
font-size 14px;
color #989898
cursor pointer
li
display table
margin-bottom: 10px;
line-height: 1em;
text-align: left;
font-size: 14px;
color: #8599ad;
transition: .2s;
cursor pointer
&:hover
color var(--main-6)
text-decoration: underline;
&:before
content '- '
.acitve
color var(--main-6)
.toc-tag
width 40px
height 40px
position fixed
right 20px
bottom 85px
z-index 999
background #585d5d
display flex
align-items: center;
justify-content: center;
flex-flow: column;
transition all .3s
cursor pointer
&:hover
background-image: linear-gradient(to right, #8EC5FC,#9FACE6)
i:nth-child(1)
transform translateX(2px)
i:nth-child(3)
transform translateX(-2px)
i
display: block;
width: 24px;
height: 2px;
background-color: hsla(0,0%,100%,.75);
margin: 3px 0;
transition: all .2s ease-in-out;
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active for below version 2.1.8 */ {
transform: translateX(10px);
opacity: 0;
}
</style>
更多推荐
所有评论(0)