前言
對於我來說一個部落格系統就是用來總結自己所學得知識的。寫寫文章,鞏固技術,寫文章我就採用了mavon-editor
,在後臺將寫好的Markdown文章儲存到資料庫裡,前臺在獲取Markdown文章將其解析成html程式碼然後渲染。所以寫文章不用愁了,那如何解析Markdown呢!我前前後後用了mavon-editor
(包太大),vue-marked
(功能少)等等外掛來實現!結果不滿足預期。所以不如使用marked.js直接解析呢!包小效率高,於是就對marked.js進行封裝,實現了目錄,執行程式碼塊,圖片檢視等功能!已經能滿足了基本的需求。
完成功能:markdown解析,防止惡意程式碼注入,生成目錄,檢視圖片,可以執行js程式碼用來展示Demo
原始碼:gitee.com/baymaxsjj/by-vue-blog/bl...
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>
本作品採用《CC 協議》,轉載必須註明作者和本文連結