使用 java 實現一個簡單的 markdown 語法解析器

1. 什麼是 markdown

Markdown 是一種輕量級的「標記語言」,它的優勢不少,目前也被愈來愈多的寫做愛好者,撰稿者普遍使用。看到這裏請不要被「標記」、「語言」所迷惑,Markdown 的語法十分簡單。經常使用的標記符號也不超過十個,這種相對於更爲複雜的HTML 標記語言來講,Markdown 可謂是十分輕量的,學習成本也不須要太多,且一旦熟悉這種語法規則,會有一勞永逸的效果。html

2. 使用 java 實現一個簡單的 markdown 語法解析器

markdown 語法解析器,能夠實現將 markdown 語句轉換成對應的 html 語句,以後由瀏覽器負責對 html 渲染。java

2.1 markdown 標籤簡介

markdown 語法十分簡單,經常使用的標籤有:git

代碼        (```)
引用        (>)
無序列表    ('*','-','+')
有序列表    ('1.','2.','3.')
標題        (#)
圖片        (![]())
連接        ([]())
行內引用    (`)
粗體        (**)
斜體        (*)
表格

具體的使用方式能夠參見 Markdown——入門指南Markdown的基本語法github

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 語句所對應的 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 文件以下code

## 什麼是 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。htm

/**
 * 判斷每一段 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 格式以下,左側爲每一行的標籤類型,右側爲文件內容:blog

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 文件便可。

相關文章
相關標籤/搜索