在這個(gè)人人都是自媒體的時(shí)代,為了擴(kuò)大個(gè)人影響力同時(shí)預(yù)防文章被盜版至其他平臺(tái),多平臺(tái)發(fā)布文章就成了創(chuàng)作者們的一大痛點(diǎn),為了解決這一痛點(diǎn)就需要將文章的編輯到發(fā)布無(wú)縫集成。
現(xiàn)在要實(shí)現(xiàn)這一功能,開發(fā)一個(gè)完全可控的Markdown編輯器就是第一步。
本文源碼已上傳Github:Github hxsfx MarkdownEditor
創(chuàng)新互聯(lián)主營(yíng)溆浦網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營(yíng)網(wǎng)站建設(shè)方案,重慶APP開發(fā),溆浦h5成都微信小程序搭建,溆浦網(wǎng)站營(yíng)銷推廣歡迎溆浦等地區(qū)企業(yè)咨詢
考慮到編輯器解析渲染放在前端更合適,采用了HTML+JS+CSS實(shí)現(xiàn)Markdown編輯器模塊。
各位小伙伴可以訪問在線演示地址:https://md.hxsfx.com/
var h4_start = "#### ";
var h3_start = "### ";
var h2_start = "## ";
var h1_start = "# ";
if (textContent.startsWith(h4_start)) {
html = textContent.substring(h4_start.length, textContent.length);
tagName = "h4";
}//四級(jí)標(biāo)題
else if (textContent.startsWith(h3_start)) {
html = textContent.substring(h3_start.length, textContent.length);
tagName = "h3";
}//三級(jí)標(biāo)題
else if (textContent.startsWith(h2_start)) {
html = textContent.substring(h2_start.length, textContent.length);
tagName = "h2";
}//二級(jí)標(biāo)題
else if (textContent.startsWith(h1_start)) {
html = textContent.substring(h1_start.length, textContent.length);
tagName = "h1";
}//一級(jí)標(biāo)題
//提取強(qiáng)調(diào)語(yǔ)法
function ExtractEmphasisGrammar(html) {
//粗斜體
var html = html.replace(/\*\*\*.*?\*\*\*/g, function (strongAndem_val) {
var _strongAndem_val = strongAndem_val.substring(3, strongAndem_val.length - 3)
return CreatePreviewSectionHTML("strong,em", _strongAndem_val);
});
//粗體
var html = html.replace(/\*\*.*?\*\*/g, function (strong_val) {
var _strong_val = strong_val.substring(2, strong_val.length - 2);
return CreatePreviewSectionHTML("strong", _strong_val);
});
//斜體
var html = html.replace(/\*.*?\*/g, function (em_val) {
var _em_val = em_val.substring(1, em_val.length - 1);
return CreatePreviewSectionHTML("em", _em_val);
});
return html;
}
//根據(jù)標(biāo)簽和內(nèi)部?jī)?nèi)容生成預(yù)覽區(qū)域內(nèi)行塊html
function CreatePreviewSectionHTML(tagName, innerHTML) {
var html = innerHTML;
if (tagName == "code") {
html = html.replace(/(\s)/g, " ");//.replace("/ /g"," ");
}//將空格替換為轉(zhuǎn)義字符防止多個(gè)空格在html顯示為一個(gè)
if (tagName == "" || tagName == null || tagName == undefined) {
} else if (tagName == "hr") {
html = "
";
} else {
var start_tagName = "";
var end_tagName = "";
var tagNameSplit = tagName.split(",");
for (var i = 0; i < tagNameSplit.length; i++) {
start_tagName += "<" + tagNameSplit[i] + ">";
end_tagName = end_tagName + "" + tagNameSplit[i] + ">";
}
html = start_tagName + html + end_tagName;
}
return html;
}
var blockquote_start = ">";
if (textContent.startsWith(blockquote_start)) {
isBlockquote = true;
html = textContent.substring(blockquote_start.length, textContent.length);
tagName = "blockquote";
}//引用
功能演示
代碼分享
var olli_pattern = /^ {0,3}[1-9]*\. /; //有序列表正則表達(dá)式
var ulli_pattern = /^[ ]{0,3}(\* |- |\+ )/; //有序列表正則表達(dá)式
if (olli_pattern.test(textContent)) {
//有序列表項(xiàng)
if (textContent.startsWith(" ")) {
isOL2 = true;
} else {
isOL = true;
}
html = textContent.replace(olli_pattern, "");
tagName = "ol";
}
else if (ulli_pattern.test(textContent)) {
//無(wú)序列表項(xiàng)
if (textContent.startsWith(" ")) {
isUL2 = true;
} else {
isUL = true;
}
html = textContent.replace(ulli_pattern, "");
tagName = "ul";
}
//提取列表語(yǔ)法
function ExtractList(analysisResult, prevAnalysisResult) {
var isExtractTable = true;
if (prevAnalysisResult == null || prevAnalysisResult.ListInfo == null) {
isExtractTable = CreateListInfo(analysisResult, isExtractTable);
}//沒有上一行 或者 上一行不是列表
else {
var liHTML = analysisResult.AnalysisHTML;
if ((prevAnalysisResult.IsOL && analysisResult.IsOL) ||
(prevAnalysisResult.IsUL && analysisResult.IsUL)) {
//接著上一行繼續(xù)
analysisResult.ListInfo = prevAnalysisResult.ListInfo;
analysisResult.ListInfo.LiInfoArray.push({ LiHtml: liHTML, LiInfoArray: [] });
analysisResult.ListInfo.IsMergePrevHTML = true;
}//同為一級(jí)標(biāo)題且標(biāo)簽相同
else if ((prevAnalysisResult.IsOL && analysisResult.IsUL) ||
(prevAnalysisResult.IsUL && analysisResult.IsOL)) {
isExtractTable = CreateListInfo(analysisResult, isExtractTable);
}
else if ((prevAnalysisResult.IsOL && (analysisResult.IsOL2 || analysisResult.IsUL2)) ||
(prevAnalysisResult.IsUL && (analysisResult.IsOL2 || analysisResult.IsUL2))) {
var currentFisrtLevelLiInfoArray = prevAnalysisResult.ListInfo.LiInfoArray.slice(-1)[0];
var secondLevelLiInfoArray = currentFisrtLevelLiInfoArray.LiInfoArray;
var isFindPeer = false;
for (var i = 0; i < secondLevelLiInfoArray.length; i++) {
var _secondLevelLiInfo = secondLevelLiInfoArray[i];
if (_secondLevelLiInfo.TagName == analysisResult.TagName) {
isFindPeer = true;
_secondLevelLiInfo.LiHtmlList.push(liHTML);
}
}
if (!isFindPeer) {
secondLevelLiInfoArray.push({ TagName: analysisResult.TagName, LiHtmlList: [liHTML] });
}
analysisResult.ListInfo = prevAnalysisResult.ListInfo;
analysisResult.ListInfo.IsMergePrevHTML = true;
}
else if ((prevAnalysisResult.IsOL2 && (analysisResult.IsOL || analysisResult.IsUL)) ||
(prevAnalysisResult.IsUL2 && (analysisResult.IsOL || analysisResult.IsUL))) {
if (prevAnalysisResult.ListInfo.TagName == analysisResult.TagName) {
prevAnalysisResult.ListInfo.LiInfoArray.push({ LiHtml: liHTML, LiInfoArray: [] });
analysisResult.ListInfo = prevAnalysisResult.ListInfo;
analysisResult.ListInfo.IsMergePrevHTML = true;
}//此二級(jí)有序項(xiàng)對(duì)應(yīng)一級(jí)項(xiàng)的列表項(xiàng)跟當(dāng)前一致且為一級(jí)
else {
isExtractTable = CreateListInfo(analysisResult, isExtractTable);
}//當(dāng)前一級(jí)與上一個(gè)一級(jí)標(biāo)簽不同無(wú)法合并,再生成一個(gè)新的
}
else if ((prevAnalysisResult.IsOL2 && analysisResult.IsOL2) ||
(prevAnalysisResult.IsUL2 && analysisResult.IsUL2)) {
var currentFisrtLevelLiInfoArray = prevAnalysisResult.ListInfo.LiInfoArray.slice(-1)[0];
var secondLevelLiInfoArray = currentFisrtLevelLiInfoArray.LiInfoArray.slice(-1)[0];
secondLevelLiInfoArray.LiHtmlList.push(liHTML);
analysisResult.ListInfo = prevAnalysisResult.ListInfo;
analysisResult.ListInfo.IsMergePrevHTML = true;
}//同為二級(jí)同標(biāo)簽,直接追加
else if ((prevAnalysisResult.IsOL2 && analysisResult.IsUL2) ||
(prevAnalysisResult.IsUL2 && analysisResult.IsOL2)) {
var currentFisrtLevelLiInfoArray = prevAnalysisResult.ListInfo.LiInfoArray.slice(-1)[0];
//這兒是要新添加不同的二級(jí)標(biāo)簽跟上面不一樣所以不要屬性
currentFisrtLevelLiInfoArray.LiInfoArray.push({ TagName: analysisResult.TagName, LiHtmlList: [liHTML] });
analysisResult.ListInfo = prevAnalysisResult.ListInfo;
analysisResult.ListInfo.IsMergePrevHTML = true;
}//雖然同時(shí)二級(jí),但標(biāo)簽不同,需生成新的二級(jí)
}
return isExtractTable;
}
function CreateListInfo(analysisResult, isExtractTable) {
if (analysisResult.IsUL || analysisResult.IsOL) {
analysisResult.ListInfo = {
IsMergePrevHTML: false,
TagName: analysisResult.TagName,
LiInfoArray: [
{
LiHtml: analysisResult.AnalysisHTML,
LiInfoArray: []
//LiInfoArray: [{
// TagName: "",
// LiHtmlList: []
//}],
},
]
};
} //識(shí)別為一級(jí)無(wú)序列表項(xiàng) 或者 識(shí)別為一級(jí)有序列表項(xiàng)
else {
isExtractTable = false;
} //第一行識(shí)別為二級(jí)列表項(xiàng),做普通處理
return isExtractTable;
}
var isCodeBlock = false;
if (analysisResult.IsCodeBlock) {
isCodeBlock = analysisResult.IsCodeBlock;
//當(dāng)前是代碼塊結(jié)束或開始
if (prevAnalysisResult == null) {
//代碼塊開始
analysisResult.TextContent = "";
previewHTMLArray.push(CreatePreviewSectionHTML("pre", analysisResult.TextContent));
}
else {
if (prevAnalysisResult.IsCodeBlock) {
//結(jié)束
analysisResult.IsCodeBlock = false;
analysisResult.TextContent = prevAnalysisResult.TextContent;
previewHTMLArray[previewHTMLArray.length - 1] = CreatePreviewSectionHTML("pre", analysisResult.TextContent);
}
else {
//開始
analysisResult.TextContent = "";
previewHTMLArray.push(CreatePreviewSectionHTML("pre", analysisResult.TextContent));
}
}
}
if (prevAnalysisResult.IsCodeBlock) {
//當(dāng)前在代碼塊內(nèi)
isCodeBlock = true;
analysisResult.IsCodeBlock = true;
var _textContent = "";
if (analysisResult.TextContent != "" && analysisResult.TextContent != null) {
_textContent = CreatePreviewSectionHTML("code", analysisResult.TextContent);
}
analysisResult.TextContent = prevAnalysisResult.TextContent + _textContent;
previewHTMLArray[previewHTMLArray.length - 1] = CreatePreviewSectionHTML("pre", analysisResult.TextContent);
}
var separator_pattern = /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/;//分隔線正則表達(dá)式
if (separator_pattern.test(textContent)) {
tagName = "hr";
}//分隔線
var def_pattern = /^ {0,3}\[(label)\]: *\n? *([^\s>]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/;
if (def_pattern.test(textContent)) {
html = "textContent";
tagName = "a";
}//鏈接
//提取鏈接和圖片語(yǔ)法
function ExtractLink(html) {
var link_pattern = /!?\[(?:\[[^\[\]]*\]|\\.|`[^`]*`|[^\[\]\\`])*?\]\(\s*(?:<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*)(?:\s+(?:"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)))?\s*\)/g;
if (link_pattern.test(html)) {
var link_pattern2 = /^!?\[((?:\[[^\[\]]*\]|\\.|`[^`]*`|[^\[\]\\`])*?)\]\(\s*(<(?:\\[<>]?|[^\s<>\\])*>|[^\s\x00-\x1f]*)(?:\s+("(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)))?\s*\)/;
html = html.replace(link_pattern, function (val) {
var getVals = link_pattern2.exec(val);
var text = getVals[1];
var href = getVals[2];
href = href.trim().replace(/^<([\s\S]*)>$/, '$1');
href = href.replace(//, "***");
href = href.replace(/<\/strong><\/em>/, "***");
href = href.replace(//, "**");
href = href.replace(/<\/strong>/, "**");
href = href.replace(//, "*");
href = href.replace(/<\/em>/, "*");
var title = getVals[3];
title = getVals[3] ? getVals[3].slice(1, -1) : '';
title = title.replace(//, "***");
title = title.replace(/<\/strong><\/em>/, "***");
title = title.replace(//, "**");
title = title.replace(/<\/strong>/, "**");
title = title.replace(//, "*");
title = title.replace(/<\/em>/, "*");
if (getVals[0].startsWith("!")) {
return "";
} else {
var text = ExtractLink(text);
return "" + text + "";
}
});
}
return html;
}
var table_tag_pattern = /^[ ]{0,3}((\|[ ]*?(?:[:]{0,1}- *){3,}[:]{0,1}[ ]*)+\|){1,}[ | ]{0,}$/;//表格出現(xiàn)的標(biāo)識(shí)
if (table_tag_pattern.test(textContent)) {
html = textContent;
}//表格
//提取表格語(yǔ)法
function ExtractTable(analysisResult, prevAnalysisResult) {
var isExtractTable = true;
if (prevAnalysisResult == null) {
isExtractTable = false;
}//但因?yàn)槭堑谝恍?,所以不能轉(zhuǎn)為表格
else {
//把當(dāng)前內(nèi)容根據(jù)|分隔進(jìn)行拆分
var currentSplitArray = analysisResult.AnalysisHTML.split('|');
if (analysisResult.IsTable && prevAnalysisResult.TableInfo == null) {
if (/^[ ]{0,3}\|/.test(prevAnalysisResult.AnalysisHTML)) {
var headerTHTextArray = prevAnalysisResult.AnalysisHTML.split('|');
analysisResult.TableInfo = {};
analysisResult.TableInfo.TextAlignArray = new Array();
var columnCount = currentSplitArray.length - 2;
if (columnCount > headerTHTextArray.length - 2) {
columnCount = headerTHTextArray.length - 2;
}
var thHTML = "";
for (var i = 0; i < columnCount; i++) {
var _tagText = currentSplitArray[i + 1].trim();
var textAlign = "";
if (_tagText.startsWith("-") && _tagText.endsWith("-")) {
textAlign = ""
} else if (_tagText.startsWith(":") && _tagText.endsWith(":")) {
textAlign = "center";
} else if (_tagText.startsWith(":")) {
textAlign = "left";
} else if (_tagText.endsWith(":")) {
textAlign = "right";
}
var textAlignStyle = ""
if (textAlign != "") {
textAlignStyle = "style=\"text-align:" + textAlign + "\";";
}
analysisResult.TableInfo.TextAlignArray.push(textAlignStyle);
thHTML += "" + headerTHTextArray[i + 1] + " "
}
analysisResult.TableInfo.THeadHTML = "" + thHTML + " ";
analysisResult.TableInfo.TBodyHTMLArray = new Array();
}//檢查上一行是不是符合做表頭內(nèi)容文本的格式條件
else {
isExtractTable = false;
}//此行雖然是表格標(biāo)識(shí)出現(xiàn),但因?yàn)樯弦恍懈袷讲粚?duì),不滿足生成表格的條件
}//當(dāng)表頭還未生成的時(shí)候先生成表頭
else if (prevAnalysisResult.TableInfo != null) {
analysisResult.TableInfo = prevAnalysisResult.TableInfo;
var tdHtml = "";
for (var i = 0; i < analysisResult.TableInfo.TextAlignArray.length; i++) {
var text = currentSplitArray[i + 1];
if (text === undefined) {
text = "";
}
tdHtml += "" + text + " "
}
analysisResult.TableInfo.TBodyHTMLArray.push("" + tdHtml + " ");
}//當(dāng)表頭生成后生成表體
else {
isExtractTable = false;
}
}
if (isExtractTable == false) {
analysisResult.TableInfo = null;
}
return isExtractTable;
}
//通過使用localStorage實(shí)現(xiàn)本地緩存
//緩存輸入至localStorage
function LocalStorageInputMD(mdInputHTML){
localStorage.setItem('ls_mdInput',mdInputHTML);
}
//實(shí)現(xiàn)輸入內(nèi)容導(dǎo)出
function exportRaw(name, data) {
var urlObject = window.URL || window.webkitURL || window;
var export_blob = new Blob([data]);
var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
save_link.href = urlObject.createObjectURL(export_blob);
save_link.download = name;
var ev = document.createEvent("MouseEvents");
ev.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
save_link.dispatchEvent(ev);
}
//通過將輸入緩存至EditorElementRecordHistoryArray中,實(shí)現(xiàn)撤銷和重做功能
//點(diǎn)擊撤銷按鈕
UndoButtonElement.onclick = function () {
//document.execCommand("Undo");
if (EditorElementRecordHistoryArray === undefined ||
EditorElementRecordHistoryArray == null) {
EditorElementRecordHistoryArray = new Array();
}
else {
if (EditorElementRecordHistoryArray.length >= 2) {
if (EditorElementRecordHistoryArray.length <= 2) {
UndoButtonElement.className += " disable";
}//當(dāng)記錄元素小于等于1個(gè),就可以撤銷了
if (EditorElementRecordHistoryArray_undo === undefined ||
EditorElementRecordHistoryArray_undo == null) {
EditorElementRecordHistoryArray_undo = new Array();
}//先判斷重做隊(duì)列是否為null
RedoButtonElement.className = RedoButtonElement.className.replace("disable", "");//將重做按鈕點(diǎn)亮
EditorElementRecordHistoryArray_undo.push(EditorElementRecordHistoryArray.pop());//將保存的歷史記錄最后一條取出來(lái)(這是當(dāng)前條,取出來(lái)要放到重做隊(duì)列)
editDivElement.innerHTML = EditorElementRecordHistoryArray.pop();//將當(dāng)前條的前一條取出來(lái)
Preview();//渲染
}
}
}
//點(diǎn)擊重做按鈕
RedoButtonElement.onclick = function () {
//document.execCommand("Redo");
if (EditorElementRecordHistoryArray_undo !== undefined &&
EditorElementRecordHistoryArray_undo != null &&
EditorElementRecordHistoryArray_undo.length > 0) {
editDivElement.innerHTML = EditorElementRecordHistoryArray_undo.pop();
Preview();//渲染
}
if (EditorElementRecordHistoryArray_undo === undefined ||
EditorElementRecordHistoryArray_undo == null ||
EditorElementRecordHistoryArray_undo.length <= 0) {
if (RedoButtonElement.className.indexOf("disable") < 0) {
RedoButtonElement.className += " disable";
}
}
}
//只要有輸入動(dòng)作就清空重做記錄
function ClearUndo() {
EditorElementRecordHistoryArray_undo = new Array();
if (RedoButtonElement.className.indexOf("disable") < 0) {
RedoButtonElement.className += " disable";
}
}
本次開發(fā)代碼質(zhì)量只能說(shuō)有手就行哈哈。接下來(lái)除了完成新功能的添加,也會(huì)預(yù)留一部分的時(shí)間來(lái)重構(gòu)代碼。如果各位小伙伴有什么建議的可以通過評(píng)論或者私信的方式告訴我,讓我們一起學(xué)習(xí)吧。
后期將對(duì)接各平臺(tái)發(fā)布功能(初步預(yù)計(jì)5個(gè)平臺(tái),包括博客園、知乎、今日頭條、CSDN、簡(jiǎn)書),預(yù)期1個(gè)月左右完成一個(gè)平臺(tái)對(duì)接,爭(zhēng)取春節(jié)前完成5個(gè)平臺(tái)的一鍵發(fā)布功能。