重庆分公司,新征程启航

为企业提供网站建设、域名注册、服务器等服务

解决自媒体一键多平台发布,从零开发Markdown编辑器(一)

前言

在这个人人都是自媒体的时代,为了扩大个人影响力同时预防文章被盗版至其他平台,多平台发布文章就成了创作者们的一大痛点,为了解决这一痛点就需要将文章的编辑到发布无缝集成。
现在要实现这一功能,开发一个完全可控的Markdown编辑器就是第一步。
本文源码已上传Github:Github hxsfx MarkdownEditor

创新互联主营溆浦网站建设的网络公司,主营网站建设方案,重庆APP开发,溆浦h5成都微信小程序搭建,溆浦网站营销推广欢迎溆浦等地区企业咨询

界面草图

技术选型

考虑到编辑器解析渲染放在前端更合适,采用了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 = "
"; } 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 + ""; } 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? *]+)>?(?:(?: +\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(//, "***");
            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;
}

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 += "" + headerTHTextArray[i + 1] + ""
                }
                analysisResult.TableInfo.THeadHTML = "" + thHTML + "";
                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 += "" + text + ""
            }
            analysisResult.TableInfo.TBodyHTMLArray.push("" + tdHtml + "");
        }//当表头生成后生成表体
        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个平台的一键发布功能。


分享文章:解决自媒体一键多平台发布,从零开发Markdown编辑器(一)
网站链接:http://cqcxhl.com/article/dsojgeg.html