前言 在這個人人都是自媒體的時代,為了擴大個人影響力同時預防文章被盜版至其他平臺,多平臺發佈文章就成了創作者們的一大痛點,為瞭解決這一痛點就需要將文章的編輯到發佈無縫集成。 現在要實現這一功能,開發一個完全可控的Markdown編輯器就是第一步。 本文源碼已上傳Github:Github hxsfx ...
前言
在這個人人都是自媒體的時代,為了擴大個人影響力同時預防文章被盜版至其他平臺,多平臺發佈文章就成了創作者們的一大痛點,為瞭解決這一痛點就需要將文章的編輯到發佈無縫集成。
現在要實現這一功能,開發一個完全可控的Markdown編輯器就是第一步。
本文源碼已上傳Github:Github hxsfx MarkdownEditor
界面草圖
技術選型
考慮到編輯器解析渲染放在前端更合適,採用了HTML+JS+CSS實現Markdown編輯器模塊。
功能演示及代碼分享
各位小伙伴可以訪問線上演示地址:https://md.hxsfx.com/
1、標題語法
- 功能演示
- 代碼分享
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";
}//四級標題
else if (textContent.startsWith(h3_start)) {
html = textContent.substring(h3_start.length, textContent.length);
tagName = "h3";
}//三級標題
else if (textContent.startsWith(h2_start)) {
html = textContent.substring(h2_start.length, textContent.length);
tagName = "h2";
}//二級標題
else if (textContent.startsWith(h1_start)) {
html = textContent.substring(h1_start.length, textContent.length);
tagName = "h1";
}//一級標題
2、強調語法
- 功能演示
- 代碼分享
//提取強調語法
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;
}
//根據標簽和內部內容生成預覽區域內行塊html
function CreatePreviewSectionHTML(tagName, innerHTML) {
var html = innerHTML;
if (tagName == "code") {
html = html.replace(/(\s)/g, " ");//.replace("/ /g"," ");
}//將空格替換為轉義字元防止多個空格在html顯示為一個
if (tagName == "" || tagName == null || tagName == undefined) {
} else if (tagName == "hr") {
html = "<hr>";
} 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;
}
3、引用語法
- 功能演示
- 代碼分享
var blockquote_start = ">";
if (textContent.startsWith(blockquote_start)) {
isBlockquote = true;
html = textContent.substring(blockquote_start.length, textContent.length);
tagName = "blockquote";
}//引用
4、列表語法
-
功能演示
-
代碼分享
var olli_pattern = /^ {0,3}[1-9]*\. /; //有序列表正則表達式
var ulli_pattern = /^[ ]{0,3}(\* |- |\+ )/; //有序列表正則表達式
if (olli_pattern.test(textContent)) {
//有序列表項
if (textContent.startsWith(" ")) {
isOL2 = true;
} else {
isOL = true;
}
html = textContent.replace(olli_pattern, "");
tagName = "ol";
}
else if (ulli_pattern.test(textContent)) {
//無序列表項
if (textContent.startsWith(" ")) {
isUL2 = true;
} else {
isUL = true;
}
html = textContent.replace(ulli_pattern, "");
tagName = "ul";
}
//提取列表語法
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)) {
//接著上一行繼續
analysisResult.ListInfo = prevAnalysisResult.ListInfo;
analysisResult.ListInfo.LiInfoArray.push({ LiHtml: liHTML, LiInfoArray: [] });
analysisResult.ListInfo.IsMergePrevHTML = true;
}//同為一級標題且標簽相同
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;
}//此二級有序項對應一級項的列表項跟當前一致且為一級
else {
isExtractTable = CreateListInfo(analysisResult, isExtractTable);
}//當前一級與上一個一級標簽不同無法合併,再生成一個新的
}
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;
}//同為二級同標簽,直接追加
else if ((prevAnalysisResult.IsOL2 && analysisResult.IsUL2) ||
(prevAnalysisResult.IsUL2 && analysisResult.IsOL2)) {
var currentFisrtLevelLiInfoArray = prevAnalysisResult.ListInfo.LiInfoArray.slice(-1)[0];
//這兒是要新添加不同的二級標簽跟上面不一樣所以不要屬性
currentFisrtLevelLiInfoArray.LiInfoArray.push({ TagName: analysisResult.TagName, LiHtmlList: [liHTML] });
analysisResult.ListInfo = prevAnalysisResult.ListInfo;
analysisResult.ListInfo.IsMergePrevHTML = true;
}//雖然同時二級,但標簽不同,需生成新的二級
}
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: []
//}],
},
]
};
} //識別為一級無序列表項 或者 識別為一級有序列表項
else {
isExtractTable = false;
} //第一行識別為二級列表項,做普通處理
return isExtractTable;
}
5、代碼塊語法
- 功能演示
- 代碼分享
var isCodeBlock = false;
if (analysisResult.IsCodeBlock) {
isCodeBlock = analysisResult.IsCodeBlock;
//當前是代碼塊結束或開始
if (prevAnalysisResult == null) {
//代碼塊開始
analysisResult.TextContent = "";
previewHTMLArray.push(CreatePreviewSectionHTML("pre", analysisResult.TextContent));
}
else {
if (prevAnalysisResult.IsCodeBlock) {
//結束
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) {
//當前在代碼塊內
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);
}
6、分隔線語法
- 功能演示
- 代碼分享
var separator_pattern = /^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/;//分隔線正則表達式
if (separator_pattern.test(textContent)) {
tagName = "hr";
}//分隔線
7、鏈接語法
- 功能演示
- 代碼分享
var def_pattern = /^ {0,3}\[(label)\]: *\n? *<?([^\s>]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/;
if (def_pattern.test(textContent)) {
html = "textContent";
tagName = "a";
}//鏈接
//提取鏈接和圖片語法
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(/<strong><em>/, "***");
href = href.replace(/<\/strong><\/em>/, "***");
href = href.replace(/<strong>/, "**");
href = href.replace(/<\/strong>/, "**");
href = href.replace(/<em>/, "*");
href = href.replace(/<\/em>/, "*");
var title = getVals[3];
title = getVals[3] ? getVals[3].slice(1, -1) : '';
title = title.replace(/<strong><em>/, "***");
title = title.replace(/<\/strong><\/em>/, "***");
title = title.replace(/<strong>/, "**");
title = title.replace(/<\/strong>/, "**");
title = title.replace(/<em>/, "*");
title = title.replace(/<\/em>/, "*");
if (getVals[0].startsWith("!")) {
return "<img src=\"" + href + "\" title=\"" + title + "\" alt=\"" + text + "\"/>";
} else {
var text = ExtractLink(text);
return "<a href=\"" + href + "\" title=\"" + title + "\">" + text + "</a>";
}
});
}
return html;
}
8、圖片語法
- 功能演示
- 代碼分享
參考第7點的鏈接語法
9、表格語法
- 功能演示
- 代碼分享
var table_tag_pattern = /^[ ]{0,3}((\|[ ]*?(?:[:]{0,1}- *){3,}[:]{0,1}[ ]*)+\|){1,}[ | ]{0,}$/;//表格出現的標識
if (table_tag_pattern.test(textContent)) {
html = textContent;
}//表格
//提取表格語法
function ExtractTable(analysisResult, prevAnalysisResult) {
var isExtractTable = true;
if (prevAnalysisResult == null) {
isExtractTable = false;
}//但因為是第一行,所以不能轉為表格
else {
//把當前內容根據|分隔進行拆分
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 += "<th " + textAlignStyle + ">" + headerTHTextArray[i + 1] + "</th>"
}
analysisResult.TableInfo.THeadHTML = "<thead><tr>" + thHTML + "</tr></thead>";
analysisResult.TableInfo.TBodyHTMLArray = new Array();
}//檢查上一行是不是符合做表頭內容文本的格式條件
else {
isExtractTable = false;
}//此行雖然是表格標識出現,但因為上一行格式不對,不滿足生成表格的條件
}//當表頭還未生成的時候先生成表頭
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 += "<td " + analysisResult.TableInfo.TextAlignArray[i] + ">" + text + "</td>"
}
analysisResult.TableInfo.TBodyHTMLArray.push("<tr>" + tdHtml + "</tr>");
}//當表頭生成後生成表體
else {
isExtractTable = false;
}
}
if (isExtractTable == false) {
analysisResult.TableInfo = null;
}
return isExtractTable;
}
10、其他功能
- 功能演示
- 代碼分享
//通過使用localStorage實現本地緩存
//緩存輸入至localStorage
function LocalStorageInputMD(mdInputHTML){
localStorage.setItem('ls_mdInput',mdInputHTML);
}
//實現輸入內容導出
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中,實現撤銷和重做功能
//點擊撤銷按鈕
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";
}//當記錄元素小於等於1個,就可以撤銷了
if (EditorElementRecordHistoryArray_undo === undefined ||
EditorElementRecordHistoryArray_undo == null) {
EditorElementRecordHistoryArray_undo = new Array();
}//先判斷重做隊列是否為null
RedoButtonElement.className = RedoButtonElement.className.replace("disable", "");//將重做按鈕點亮
EditorElementRecordHistoryArray_undo.push(EditorElementRecordHistoryArray.pop());//將保存的歷史記錄最後一條取出來(這是當前條,取出來要放到重做隊列)
editDivElement.innerHTML = EditorElementRecordHistoryArray.pop();//將當前條的前一條取出來
Preview();//渲染
}
}
}
//點擊重做按鈕
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";
}
}
}
//只要有輸入動作就清空重做記錄
function ClearUndo() {
EditorElementRecordHistoryArray_undo = new Array();
if (RedoButtonElement.className.indexOf("disable") < 0) {
RedoButtonElement.className += " disable";
}
}
寫在最後
本次開發代碼質量只能說有手就行哈哈。接下來除了完成新功能的添加,也會預留一部分的時間來重構代碼。如果各位小伙伴有什麼建議的可以通過評論或者私信的方式告訴我,讓我們一起學習吧。
預告一下
後期將對接各平臺發佈功能(初步預計5個平臺,包括博客園、知乎、今日頭條、CSDN、簡書),預期1個月左右完成一個平臺對接,爭取春節前完成5個平臺的一鍵發佈功能。
博客園-本文作者(好先生FX http://www.cnblogs.com/hxsfx)
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。