Commit 33258e68bd5a0c7170b0badbcd271423f6decc7b

Martin Mitas 2016-10-04T21:18:30

Implement block quotes.

diff --git a/README.md b/README.md
index 8557d4b..270069a 100644
--- a/README.md
+++ b/README.md
@@ -92,7 +92,7 @@ more or less forms our to do list.
   - [x] 4.9 Blank lines
 
 - **Container Blocks:**
-  - [ ] 5.1 Block quotes
+  - [x] 5.1 Block quotes
   - [ ] 5.2 List items
   - [ ] 5.3 Lists
 
diff --git a/md2html/md2html.c b/md2html/md2html.c
index c062e2f..1b79cc5 100644
--- a/md2html/md2html.c
+++ b/md2html/md2html.c
@@ -148,6 +148,7 @@ enter_block_callback(MD_BLOCKTYPE type, void* detail, void* userdata)
 
     switch(type) {
         case MD_BLOCK_DOC:      /* noop */ break;
+        case MD_BLOCK_QUOTE:    MEMBUF_APPEND_LITERAL(out, "<blockquote>"); break;
         case MD_BLOCK_HR:       MEMBUF_APPEND_LITERAL(out, "<hr>\n"); break;
         case MD_BLOCK_H:        MEMBUF_APPEND_LITERAL(out, head[((MD_BLOCK_H_DETAIL*)detail)->level - 1]); break;
         case MD_BLOCK_CODE:     open_code_block(out, (const MD_BLOCK_CODE_DETAIL*) detail); break;
@@ -166,6 +167,7 @@ leave_block_callback(MD_BLOCKTYPE type, void* detail, void* userdata)
 
     switch(type) {
         case MD_BLOCK_DOC:      /*noop*/ break;
+        case MD_BLOCK_QUOTE:    MEMBUF_APPEND_LITERAL(out, "</blockquote>"); break;
         case MD_BLOCK_HR:       /*noop*/ break;
         case MD_BLOCK_H:        MEMBUF_APPEND_LITERAL(out, head[((MD_BLOCK_H_DETAIL*)detail)->level - 1]); break;
         case MD_BLOCK_CODE:     MEMBUF_APPEND_LITERAL(out, "</code></pre>\n"); break;
diff --git a/md4c/md4c.c b/md4c/md4c.c
index 05f8775..c0ac87a 100644
--- a/md4c/md4c.c
+++ b/md4c/md4c.c
@@ -76,6 +76,9 @@ struct MD_CTX_tag {
     MD_RENDERER r;
     void* userdata;
 
+    /* For MD_BLOCK_QUOTE */
+    unsigned quote_level;   /* Nesting level. */
+
     /* Minimal indentation to call the block "indented code". */
     unsigned code_indent_offset;
 
@@ -112,6 +115,7 @@ struct MD_LINE_tag {
     MD_LINETYPE type;
     OFF beg;
     OFF end;
+    unsigned quote_level;   /* Level of nesting in <blockquote>. */
     unsigned indent;        /* Indentation level. */
 };
 
@@ -667,8 +671,10 @@ md_analyze_line(MD_CTX* ctx, OFF beg, OFF* p_end, const MD_LINE* pivot_line, MD_
     OFF off = beg;
 
     line->type = MD_LINE_BLANK;
+    line->quote_level = 0;
     line->indent = 0;
 
+redo_indentation_after_blockquote_mark:
     /* Eat indentation. */
     while(off < ctx->size  &&  ISBLANK(off)) {
         if(CH(off) == _T('\t'))
@@ -706,6 +712,16 @@ md_analyze_line(MD_CTX* ctx, OFF beg, OFF* p_end, const MD_LINE* pivot_line, MD_
         goto done;
     }
 
+    /* Check blockquote mark. */
+    if(off < ctx->size  &&  CH(off) == _T('>')) {
+        off++;
+        if(off < ctx->size  &&  CH(off) == _T(' '))
+            off++;
+        line->quote_level++;
+        line->indent = 0;
+        goto redo_indentation_after_blockquote_mark;
+    }
+
     /* Check whether we are blank line.
      * Note blank lines after indented code are treated as part of that block.
      * If they are at the end of the block, it is discarded by caller.
@@ -787,6 +803,11 @@ md_analyze_line(MD_CTX* ctx, OFF beg, OFF* p_end, const MD_LINE* pivot_line, MD_
     /* By default, we are normal text line. */
     line->type = MD_LINE_TEXT;
 
+    /* Ordinary text line may need to upgrade block quote level because
+     * of its lazy continuation. */
+    if(pivot_line->type == MD_LINE_TEXT  &&  pivot_line->quote_level > line->quote_level)
+        line->quote_level = pivot_line->quote_level;
+
 done:
     /* Eat rest of the line contents */
     while(off < ctx->size  &&  !ISNEWLINE(off))
@@ -819,6 +840,27 @@ done:
     *p_end = off;
 }
 
+static int
+md_process_blockquote_nesting(MD_CTX* ctx, unsigned desired_level)
+{
+    int ret = 0;
+
+    /* Bring blockquote nesting to expected level. */
+    if(ctx->quote_level != desired_level) {
+        while(ctx->quote_level < desired_level) {
+            MD_ENTER_BLOCK(MD_BLOCK_QUOTE, NULL);
+            ctx->quote_level++;
+        }
+        while(ctx->quote_level > desired_level) {
+            MD_LEAVE_BLOCK(MD_BLOCK_QUOTE, NULL);
+            ctx->quote_level--;
+        }
+    }
+
+abort:
+    return ret;
+}
+
 /* Determine type of the block (from type of its 1st line and some context),
  * call block_enter() callback, then appropriate function to parse contents
  * of the block, and finally block_leave() callback.
@@ -836,6 +878,12 @@ md_process_block(MD_CTX* ctx, const MD_LINE* lines, int n_lines)
     if(n_lines == 0)
         return 0;
 
+    /* Make sure the processed leaf block lives in the proper block quote
+     * nesting level. */
+    ret = md_process_blockquote_nesting(ctx, lines[0].quote_level);
+    if(ret != 0)
+        goto abort;
+
     /* Derive block type from type of the first line. */
     switch(lines[0].type) {
         case MD_LINE_BLANK:
@@ -973,8 +1021,9 @@ md_process_doc(MD_CTX *ctx)
             line->type = MD_LINE_BLANK;
         }
 
-        /* New block also starts if line type changes. */
-        if(line->type != pivot_line->type) {
+        /* New block also starts if line type changes or if block quote nesting
+         * level changes. */
+        if(line->type != pivot_line->type  ||  line->quote_level != pivot_line->quote_level) {
             ret = md_process_block(ctx, lines, n_lines);
             if(ret != 0)
                 goto abort;
@@ -1002,6 +1051,11 @@ md_process_doc(MD_CTX *ctx)
             goto abort;
     }
 
+    /* Close any dangling parent blocks. */
+    ret = md_process_blockquote_nesting(ctx, 0);
+    if(ret != 0)
+        goto abort;
+
     MD_LEAVE_BLOCK(MD_BLOCK_DOC, NULL);
 
 abort:
diff --git a/md4c/md4c.h b/md4c/md4c.h
index 6948adb..b0d577c 100644
--- a/md4c/md4c.h
+++ b/md4c/md4c.h
@@ -61,6 +61,9 @@ enum MD_BLOCKTYPE_tag {
     /* <body>...</body> */
     MD_BLOCK_DOC = 0,
 
+    /* <blockquote>...</blockquote> */
+    MD_BLOCK_QUOTE,
+
     /* <hr> */
     MD_BLOCK_HR,