使用 java 实现一个简单的 markdown 语法解析器

1. 什么是 markdown

Markdown 是一种轻量级的「标记语言」,它的优点很多,目前也被越来越多的写作爱好者,撰稿者广泛使用。看到这里请不要被「标记」、「语言」所迷惑,Markdown 的语法十分简单。常用的标记符号也不超过十个,这种相对于更为复杂的HTML 标记语言来说,Markdown 可谓是十分轻量的,学习成本也不需要太多,且一旦熟悉这种语法规则,会有一劳永逸的效果。

2. 使用 java 实现一个简单的 markdown 语法解析器

markdown 语法解析器,可以实现将 markdown 语句转换成对应的 html 语句,之后由浏览器负责对 html 渲染。

2.1 markdown 标签简介

markdown 语法十分简单,常用的标签有:

代码        (```)
引用        (>)
无序列表    ('*','-','+')
有序列表    ('1.','2.','3.')
标题        (#)
图片        (![]())
链接        ([]())
行内引用    (`)
粗体        (**)
斜体        (*)
表格

具体的使用方式可以参见 Markdown——入门指南Markdown的基本语法

2.2 markdown 标签分类

  • markdown 标签可以简单分为 2 大类:一类是作用在多行语句或单行语句中的,如 代码、引用、无序列表、有序列表、表格等;另一类是只作用于单行语句中的,如 标题,图片,链接、行内引用、粗体、斜体等。
  • 其中,代码、引用、无序列表、有序列表、标题 这 5 类可以直接根据行首是否存在相应的标签直接进行判断这一行是否属于这些类型。如 行首元素为 '>' 可以直接判断这是一个引用行,行首元素为 '-' 可以直接判断这是一个无序列表行等。但需要注意的是,代码区域内不存在其他元素,即代码区域内的其他标签并不会被解析;而引用区域内可以存在其他元素,如 行首元素为 "> *" 可以判断此行为一个引用区域内的无序列表行。
  • 除了这 5 类标签外,图片,链接、行内引用、粗体、斜体这 5 类标签可以出现在行内的任意位置,于是要遍历一整行才可以解析出这 5 类标签。

2.3 markdown 解析器的实现

完整代码见 https://github.com/libaoquan95/MarkDownParser
已实现的 markdown 标签:代码、引用、无序列表、有序列表、标题、普通文本、图片、链接、行内引用、粗体、斜体
未实现的 markdown 标签:表格、tab多级结构

2.3.1 主体思路

主要思路是扫描 markdown 文件,对每一行进行标记,确定每一行的 markdown 标签,之后再根据每一行的 markdown 标签将 markdown 语句转换成 html 语句。

  • 第一次扫描 markdown 文件,定位 代码区引用区无序列表区有序列表区,因为这些标签均是可以作用于多行,要根据上下文的 markdown 标签才可以确定其作用范围。在这里需要特别注意 代码区 内不含其它区域,引用区 内可以嵌套其它区域。
  • 第二次扫描 markdown 文件,根据前一次扫描的定位结果,确定每一行 markdown 语句所对应的 markdown 标签。在这次扫描中可以确定 代码引用无序列表有序列表标题 这 5 类可以根据行首元素就能判定出类型的标签,所以不需要扫描全行。
  • 第三次扫描 markdown 文件,根据上一次的结果,可以直接将对应的 markdown 标签转换成 html 标签,此外要扫描全行,确定 图片链接行内引用粗体斜体 这 5 类元素并直接转换成 html。

2.3.2 读入 markdown 文件

扫描文件后,将文件按行存储至内存。相关成员变量如下:

// 按行存储 markdown 文件
private ArrayList<String> mdList = new ArrayList();
// 存储 markdown 文件的每一行对应类型
private ArrayList<String> mdListType = new ArrayList();

将 markdown 文件存入 mdList,之后多次扫描均是直接在 mdList 上进行修改。在本博客中,展示的事例的 markdown 文件如下

    ## 什么是 markdown 
    > Markdown 是一种轻量级的「标记语言」,它的优点很多,目前也被越来越多的写作爱好者...

    ## markdown 常用标签:
    ```
    代码        (```)
    引用        (>)
    无序列表    ('*','-','+')
    有序列表    ('1.','2.','3.')
    标题        (#)
    图片        (![]())
    链接        ([]())
    行内引用    (`)
    粗体        (**)
    斜体        (*)
    表格
    ```
    ## markdown 入门1 
    1. [Markdown——入门指南](http://www.jianshu.com/p/1e402922ee32/)
    2. [Markdown的基本语法](http://www.cnblogs.com/libaoquan/p/6812426.html)

    ### markdown 标签分类
    - markdown 标签可以 **简单** 分为 2 大类:...
    - 其中,*代码*、`引用`、无序列表、有序列表、标题这 5 类...
    - 除了这 5 类标签外,图片,链接、行内引用、粗体、斜体这 5 类...

2.3.3 第一次扫描

在这次扫描中,可以确定出 代码区 定位标签 CODE_BEGIN 和 CODE_END,引用区 定位标签 QUOTE_BEGIN 和 QUOTE_END,无序列表区 定位标签 UNORDER_BEGIN 和 UNORDER_END,有序列表区 定位标签 ORDER_BEGIN 和 ORDER_END。而其他语句在此次扫描中均暂时定义为 OTHER。

/**
 * 判断每一段 markdown 语法对应的 html 类型
 * @param 空
* @return 空
 */
private void defineAreaType() {
    // 定位代码区
    ArrayList<String> tempList = new ArrayList();
    ArrayList<String> tempType = new ArrayList();
    tempType.add("OTHER");
    tempList.add(" ");
    boolean codeBegin = false, codeEnd = false;
    for(int i = 1; i < mdList.size() - 1; i++){
        String line = mdList.get(i);
        if(line.length() > 2 && line.charAt(0) == '`' && line.charAt(1) == '`' && line.charAt(2) == '`') {
            // 进入代码区
            if(!codeBegin && !codeEnd) {
                tempType.add("CODE_BEGIN");
                tempList.add(" ");
                codeBegin = true;
            }
            // 离开代码区
            else if(codeBegin && !codeEnd) {
                tempType.add("CODE_END");
                tempList.add(" ");
                codeBegin = false;
                codeEnd = false;
            }
            else {
                tempType.add("OTHER");
                tempList.add(line);
            }
        }
        else {
            tempType.add("OTHER");
            tempList.add(line);
        }
    }
    tempType.add("OTHER");
    tempList.add(" ");

    mdList = (ArrayList<String>)tempList.clone();
    mdListType = (ArrayList<String>)tempType.clone();
    tempList.clear();
    tempType.clear();

    // 定位其他区,注意代码区内无其他格式
    boolean isCodeArea = false;
    tempList.add(" ");
    tempType.add("OTHER");
    for(int i = 1; i < mdList.size() - 1; i++){
        String line = mdList.get(i);
        String lastLine = mdList.get(i - 1);
        String nextLine = mdList.get(i + 1);

        if(mdListType.get(i) == "CODE_BEGIN") {
            isCodeArea = true;
            tempList.add(line);
            tempType.add("CODE_BEGIN");
            continue;
        }
        if(mdListType.get(i) == "CODE_END") {
            isCodeArea = false;
            tempList.add(line);
            tempType.add("CODE_END");
            continue;
        }
        
        // 代码区不含其他格式
        if(!isCodeArea) {
            // 进入引用区
            if(line.length() > 2 && line.charAt(0) == '>' && lastLine.charAt(0) != '>' && nextLine.charAt(0) == '>') {
                tempList.add(" ");
                tempList.add(line);
                tempType.add("QUOTE_BEGIN");
                tempType.add("OTHER");
            }
            // 离开引用区
            else if(line.length() > 2 && line.charAt(0) == '>' && lastLine.charAt(0) == '>' && nextLine.charAt(0) != '>') {
                tempList.add(line);
                tempList.add(" ");
                tempType.add("OTHER");
                tempType.add("QUOTE_END");
                
            }
            // 单行引用区
            else if(line.length() > 2 && line.charAt(0) == '>' && lastLine.charAt(0) != '>' && nextLine.charAt(0) != '>') {
                tempList.add(" ");
                tempList.add(line);
                tempList.add(" ");
                tempType.add("QUOTE_BEGIN");
                tempType.add("OTHER");
                tempType.add("QUOTE_END");
                
            }
            // 进入无序列表区
            else if((line.charAt(0) == '-' && lastLine.charAt(0) != '-' && nextLine.charAt(0) == '-') ||
                    (line.charAt(0) == '+' && lastLine.charAt(0) != '+' && nextLine.charAt(0) == '+') ||
                    (line.charAt(0) == '*' && lastLine.charAt(0) != '*' && nextLine.charAt(0) == '*')){
                tempList.add(" ");
                tempList.add(line);
                tempType.add("UNORDER_BEGIN");
                tempType.add("OTHER");
            }
            // 离开无序列表区
            else if((line.charAt(0) == '-' && lastLine.charAt(0) == '-' && nextLine.charAt(0) != '-') ||
                    (line.charAt(0) == '+' && lastLine.charAt(0) == '+' && nextLine.charAt(0) != '+') ||
                    (line.charAt(0) == '*' && lastLine.charAt(0) == '*' && nextLine.charAt(0) != '*')){
                tempList.add(line);
                tempList.add(" ");
                tempType.add("OTHER");
                tempType.add("UNORDER_END");
            }
            // 单行无序列表区
            else if((line.charAt(0) == '-' && lastLine.charAt(0) != '-' && nextLine.charAt(0) != '-') ||
                    (line.charAt(0) == '+' && lastLine.charAt(0) != '+' && nextLine.charAt(0) != '+') ||
                    (line.charAt(0) == '*' && lastLine.charAt(0) != '*' && nextLine.charAt(0) != '*')){
                tempList.add(" ");
                tempList.add(line);
                tempList.add(" ");
                tempType.add("UNORDER_BEGIN");
                tempType.add("OTHER");
                tempType.add("UNORDER_END");
            }
            // 进入有序列表区
            else if((line.length() > 1 && (line.charAt(0) >= '1' || line.charAt(0) <= '9')  && (line.charAt(1) == '.')) &&
                    !(lastLine.length() > 1 && (lastLine.charAt(0) >= '1' || line.charAt(0) <= '9')  && (lastLine.charAt(1) == '.')) &&
                    (nextLine.length() > 1 && (nextLine.charAt(0) >= '1' || line.charAt(0) <= '9')  && (nextLine.charAt(1) == '.'))){
                tempList.add(" ");
                tempList.add(line);
                tempType.add("ORDER_BEGIN");
                tempType.add("OTHER");
            }
            // 离开有序列表区
            else if((line.length() > 1 && (line.charAt(0) >= '1' || line.charAt(0) <= '9')  && (line.charAt(1) == '.')) &&
                    (lastLine.length() > 1 && (lastLine.charAt(0) >= '1' || line.charAt(0) <= '9')  && (lastLine.charAt(1) == '.')) &&
                    !(nextLine.length() > 1 && (nextLine.charAt(0) >= '1' || line.charAt(0) <= '9')  && (nextLine.charAt(1) == '.'))){
                tempList.add(line);
                tempList.add(" ");
                tempType.add("OTHER");
                tempType.add("ORDER_END");
            }
            // 单行有序列表区
            else if((line.length() > 1 && (line.charAt(0) >= '1' || line.charAt(0) <= '9')  && (line.charAt(1) == '.')) &&
                    !(lastLine.length() > 1 && (lastLine.charAt(0) >= '1' || line.charAt(0) <= '9')  && (lastLine.charAt(1) == '.')) &&
                    !(nextLine.length() > 1 && (nextLine.charAt(0) >= '1' || line.charAt(0) <= '9')  && (nextLine.charAt(1) == '.'))){
                tempList.add(" ");
                tempList.add(line);
                tempList.add(" ");
                tempType.add("ORDER_BEGIN");
                tempType.add("OTHER");
                tempType.add("ORDER_END");
            }
            // 其他
            else {
                tempList.add(line);
                tempType.add("OTHER");
            }
        }
        else {
            tempList.add(line);
            tempType.add("OTHER");
        }
    }
    tempList.add(" ");
    tempType.add("OTHER");
    
    mdList = (ArrayList<String>)tempList.clone();
    mdListType = (ArrayList<String>)tempType.clone();
    tempList.clear();
    tempType.clear();
}

第一次扫描后,markdown 格式如下,左侧为每一行的标签类型,右侧为文件内容:

OTHER            
OTHER           ## 什么是 markdown 
QUOTE_BEGIN      
OTHER           > Markdown 是一种轻量级的「标记语言」,它的优点很多,目前也被越来越多的写作爱好者...
QUOTE_END        
OTHER            
OTHER           ## markdown 常用标签:
CODE_BEGIN       
OTHER           代码        (```)
OTHER           引用        (>)
OTHER           无序列表    ('*','-','+')
OTHER           有序列表    ('1.','2.','3.')
OTHER           标题        (#)
OTHER           图片        (![]())
OTHER           链接        ([]())
OTHER           行内引用    (`)
OTHER           粗体        (**)
OTHER           斜体        (*)
OTHER           表格
CODE_END         
OTHER           ## markdown 入门1 
ORDER_BEGIN      
OTHER           1. [Markdown——入门指南](http://www.jianshu.com/p/1e402922ee32/)
OTHER           2. [Markdown的基本语法](http://www.cnblogs.com/libaoquan/p/6812426.html)
ORDER_END        
OTHER            
OTHER           ### markdown 标签分类
UNORDER_BEGIN    
OTHER           - markdown 标签可以 **简单** 分为 2 大类:...
OTHER           - 其中,*代码*、`引用`、无序列表、有序列表、标题这 5 类...
OTHER           - 除了这 5 类标签外,图片,链接、行内引用、粗体、斜体这 5 类...
UNORDER_END      
OTHER            

2.3.4 第二次扫描

在这次扫描中,可以确定出 代码行 标签 CODE_LINE, 无序列表行 标签 UNORDER_LINE, 有序列表行 标签 ORDER_LINE, 空行 标签 BLANK_LINE, 标题行 标签 TITLE。

/**
 * 判断每一行 markdown 语法对应的 html 类型
 * @param 空
 * @return 空
 */
private void defineLineType() {
    Stack<String> st = new Stack();
    for(int i = 0; i < mdList.size(); i++){
        String line = mdList.get(i);
        String typeLine = mdListType.get(i);
        if(typeLine == "QUOTE_BEGIN" || typeLine == "UNORDER_BEGIN" || typeLine == "ORDER_BEGIN" || typeLine == "CODE_BEGIN") {
            st.push(typeLine);
        }
        else if(typeLine == "QUOTE_END" || typeLine == "UNORDER_END" || typeLine == "ORDER_END" || typeLine == "CODE_END") {
            st.pop();
        }
        else if(typeLine == "OTHER") {
            if(!st.isEmpty()) {
                // 引用行
                if(st.peek() == "QUOTE_BEGIN") {
                    mdList.set(i, line.trim().substring(1).trim());
                }
                // 无序列表行
                else if(st.peek() == "UNORDER_BEGIN") {
                    mdList.set(i, line.trim().substring(1).trim());
                    mdListType.set(i, "UNORDER_LINE");
                }
                // 有序列表行
                else if(st.peek() == "ORDER_BEGIN") {
                    mdList.set(i, line.trim().substring(2).trim());
                    mdListType.set(i, "ORDER_LINE");
                }
                // 代码行
                else {
                    mdListType.set(i, "CODE_LINE");
                }
            }
            line = mdList.get(i);
            typeLine = mdListType.get(i);
            // 空行
            if(line.trim().isEmpty()) {
                mdListType.set(i, "BLANK_LINE");
                mdList.set(i, "");
            }
            // 标题行
            else if(line.trim().charAt(0) == '#') {
                mdListType.set(i, "TITLE");
                mdList.set(i, line.trim());
            }
        }
    }
}

第二次扫描后,markdown 格式如下,左侧为每一行的标签类型,右侧为文件内容:

BLANK_LINE      
TITLE           ## 什么是 markdown
QUOTE_BEGIN      
OTHER           Markdown 是一种轻量级的「标记语言」,它的优点很多,目前也被越来越多的写作爱好者...
QUOTE_END        
BLANK_LINE      
TITLE           ## markdown 常用标签:
CODE_BEGIN       
CODE_LINE       代码        (```)
CODE_LINE       引用        (>)
CODE_LINE       无序列表    ('*','-','+')
CODE_LINE       有序列表    ('1.','2.','3.')
CODE_LINE       标题        (#)
CODE_LINE       图片        (![]())
CODE_LINE       链接        ([]())
CODE_LINE       行内引用    (`)
CODE_LINE       粗体        (**)
CODE_LINE       斜体        (*)
CODE_LINE       表格
CODE_END         
TITLE           ## markdown 入门1
ORDER_BEGIN      
ORDER_LINE      [Markdown——入门指南](http://www.jianshu.com/p/1e402922ee32/)
ORDER_LINE      [Markdown的基本语法](http://www.cnblogs.com/libaoquan/p/6812426.html)
ORDER_END        
BLANK_LINE      
TITLE           ### markdown 标签分类
UNORDER_BEGIN    
UNORDER_LINE    markdown 标签可以 **简单** 分为 2 大类:...
UNORDER_LINE    其中,*代码*、`引用`、无序列表、有序列表、标题这 5 类...
UNORDER_LINE    除了这 5 类标签外,图片,链接、行内引用、粗体、斜体这 5 类...
UNORDER_END      
BLANK_LINE      

2.3.5 第三次扫描

在这次扫描中,根据每一行的标签,将其转化为 html 代码,并行内扫描确定 图片链接行内引用粗体斜体

/**
 * 根据每一行的类型,将 markdown 语句 转化成 html 语句
 * @return 空
 */
private void translateToHtml() {
    for(int i = 0; i < mdList.size(); i++){
        String line = mdList.get(i);
        String typeLine = mdListType.get(i);
        // 是空行
        if(typeLine == "BLANK_LINE") {
            mdList.set(i, "");
        }
        // 是普通文本行
        else if(typeLine == "OTHER") {
            mdList.set(i, "<p>" + translateToHtmlInline(line.trim()) + "</p>");
        }
        // 是标题行
        else if(typeLine == "TITLE") {
            int titleClass = 1;
            for(int j = 1; j < line.length(); j++) {
                if(line.charAt(j) == '#') {
                    titleClass++;
                }
                else {
                    break;
                }
            }
            mdList.set(i, "<h" + titleClass + ">"+ translateToHtmlInline(line.substring(titleClass).trim()) +"</h" + titleClass + ">");
        }
        // 是无序列表行
        else if(typeLine == "UNORDER_BEGIN") {
            mdList.set(i, "<ul>");
        }
        else if(typeLine == "UNORDER_END") {
            mdList.set(i, "</ul>");
        }
        else if(typeLine == "UNORDER_LINE") {
            mdList.set(i, "<li>" + translateToHtmlInline(line.trim()) + "</li>");
        }
        // 是有序列表行
        else if(typeLine == "ORDER_BEGIN") {
            mdList.set(i, "<ol>");
        }
        else if(typeLine == "ORDER_END") {
            mdList.set(i, "</ol>");
        }
        else if(typeLine == "ORDER_LINE") {
            mdList.set(i, "<li>" + translateToHtmlInline(line.trim()) + "</li>");
        }
        // 是代码行
        else if(typeLine == "CODE_BEGIN") {
            mdList.set(i, "<pre>");
        }
        else if(typeLine == "CODE_END") {
            mdList.set(i, "</pre>");
        }
        else if(typeLine == "CODE_LINE") {
            mdList.set(i, "<code>" + line + "</code>");
        }
        // 是引用行
        else if(typeLine == "QUOTE_BEGIN") {
            mdList.set(i, "<blockquote>");
        }
        else if(typeLine == "QUOTE_END"){
            mdList.set(i, "</blockquote>");
        }
    }
}

/**
 * 将行内的 markdown 语句转换成对应的 html
 * @param mark 语句
 * @return html 语句
 */
private String translateToHtmlInline( String line) {
    String html = "";
    for(int i=0; i<line.length();i++) {
        // 图片
        if(i < line.length() - 4 && line.charAt(i) == '!' && line.charAt(i + 1) == '[') {
            int index1 = line.indexOf(']', i + 1);
            if(index1 != -1 && line.charAt(index1 + 1) == '(' && line.indexOf(')', index1 + 2) != -1){
                int index2 = line.indexOf(')', index1 + 2);
                String picName = line.substring(i + 2, index1);
                String picPath = line.substring(index1 + 2, index2);
                line = line.replace(line.substring(i, index2 + 1), "<img alt='" + picName + "' src='" + picPath + "' />");
            }
        }
        // 链接
        if(i < line.length() - 3 && ((i > 0 && line.charAt(i) == '[' && line.charAt(i - 1) != '!') || (line.charAt(0) == '['))) {
            int index1 = line.indexOf(']', i + 1);
            if(index1 != -1 && line.charAt(index1 + 1) == '(' && line.indexOf(')', index1 + 2) != -1){
                int index2 = line.indexOf(')', index1 + 2);
                String linkName = line.substring(i + 1, index1);
                String linkPath = line.substring(index1 + 2, index2);
                line = line.replace(line.substring(i, index2 + 1), "<a href='" + linkPath + "'> " + linkName + "</a>");
            }
        }
        // 行内引用
        if(i < line.length() - 1 && line.charAt(i) == '`' && line.charAt(i + 1) != '`') {
            int index = line.indexOf('`', i + 1);
            if(index != -1) {
                String quoteName = line.substring(i + 1, index);
                line = line.replace(line.substring(i, index + 1), "<code>" + quoteName + "</code>");
            }
        }
        // 粗体
        if(i < line.length() - 2 && line.charAt(i) == '*' && line.charAt(i + 1) == '*') {
            int index = line.indexOf("**", i + 1);
            if(index != -1) {
                String quoteName = line.substring(i + 2, index );
                line = line.replace(line.substring(i, index + 2), "<strong>" + quoteName + "</strong>");
            }
        }
        // 斜体
        if(i < line.length() - 2 && line.charAt(i) == '*' && line.charAt(i + 1) != '*') {
            int index = line.indexOf('*', i + 1);
            if(index != -1 && line.charAt(index + 1) != '*') {
                String quoteName = line.substring(i + 1, index);
                line = line.replace(line.substring(i, index + 1), "<i>" + quoteName + "</i>");
            }
        }
    }
    return line;
}

第三次扫描后,markdown 格式如下,左侧为每一行的标签类型,右侧为文件内容:

BLANK_LINE      
TITLE           <h2>什么是 markdown</h2>
QUOTE_BEGIN     <blockquote>
OTHER           <p>Markdown 是一种轻量级的「标记语言」,它的优点很多,目前也被越来越多的写作爱好者...</p>
QUOTE_END       </blockquote>
BLANK_LINE      
TITLE           <h2>markdown 常用标签:</h2>
CODE_BEGIN      <pre>
CODE_LINE       <code>代码        (```)</code>
CODE_LINE       <code>引用        (>)</code>
CODE_LINE       <code>无序列表    ('*','-','+')</code>
CODE_LINE       <code>有序列表    ('1.','2.','3.')</code>
CODE_LINE       <code>标题        (#)</code>
CODE_LINE       <code>图片        (![]())</code>
CODE_LINE       <code>链接        ([]())</code>
CODE_LINE       <code>行内高亮    (`)</code>
CODE_LINE       <code>粗体        (**)</code>
CODE_LINE       <code>斜体        (*)</code>
CODE_LINE       <code>表格</code>
CODE_END        </pre>
TITLE           <h2>markdown 入门1</h2>
ORDER_BEGIN     <ol>
ORDER_LINE      <li><a href='http://www.jianshu.com/p/1e402922ee32/'> Markdown——入门指南</a></li>
ORDER_LINE      <li><a href='http://www.cnblogs.com/libaoquan/p/6812426.html'> Markdown的基本语法</a></li>
ORDER_END       </ol>
BLANK_LINE      
TITLE           <h3>markdown 标签分类</h3>
UNORDER_BEGIN   <ul>
UNORDER_LINE    <li>markdown 标签可以 <strong>简单</strong> 分为 2 大类:...</li>
UNORDER_LINE    <li>其中,<i>代码</i>、<code>引用</code>、无序列表、有序列表、标题这 5 类...</li>
UNORDER_LINE    <li>除了这 5 类标签外,图片,链接、行内高亮、粗体、斜体这 5 类...</li>
UNORDER_END     </ul>
BLANK_LINE      

至此,将 mdList 与 html 头部 与 尾部 写入 html 文件即可。

posted @ 2017-08-28 11:27  LiBaoquan  阅读(10671)  评论(0编辑  收藏  举报